| name | performance-security |
| description | Performance optimization, accessibility, and security best practices for React apps. Covers code-splitting, React Compiler patterns, asset optimization, a11y testing, and security hardening. Use when optimizing performance or reviewing security. |
Performance, Accessibility & Security
Production-ready patterns for building fast, accessible, and secure React applications.
Performance Optimization
Code-Splitting
Automatic with TanStack Router:
- File-based routing automatically code-splits by route
- Each route is its own chunk
- Vite handles dynamic imports efficiently
Manual code-splitting:
import { lazy, Suspense } from 'react'
// Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'))
function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart data={data} />
</Suspense>
)
}
Route-level lazy loading:
// src/routes/dashboard.lazy.tsx
export const Route = createLazyFileRoute('/dashboard')({
component: DashboardComponent,
})
React Compiler First
The React Compiler automatically optimizes performance when you write compiler-friendly code:
✅ Do:
- Keep components pure (no side effects in render)
- Derive values during render (don't stash in refs)
- Keep props serializable
- Inline event handlers (unless they close over large objects)
❌ Avoid:
- Mutating props or state
- Side effects in render phase
- Over-using useCallback/useMemo (compiler handles this)
- Non-serializable props (functions, symbols)
Verify optimization:
- Check React DevTools for "Memo ✨" badge
- Components without badge weren't optimized (check for violations)
Images & Assets
Use Vite asset pipeline:
// Imports are optimized and hashed
import logo from './logo.png'
<img src={logo} alt="Logo" />
Prefer modern formats:
// WebP for photos
<img src="/hero.webp" alt="Hero" />
// SVG for icons
import { ReactComponent as Icon } from './icon.svg'
<Icon />
Lazy load images:
<img src={imageSrc} loading="lazy" alt="Description" />
Responsive images:
<img
srcSet="
/image-320w.webp 320w,
/image-640w.webp 640w,
/image-1280w.webp 1280w
"
sizes="(max-width: 640px) 100vw, 640px"
src="/image-640w.webp"
alt="Description"
/>
Bundle Analysis
# Build with analysis
npx vite build --mode production
# Visualize bundle
pnpm add -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
react(),
visualizer({ open: true }),
],
})
Performance Checklist
- Code-split routes and heavy components
- Verify React Compiler optimizations (✨ badges)
- Optimize images (WebP, lazy loading, responsive)
- Prefetch critical data in route loaders
- Use TanStack Query for automatic deduplication
- Set appropriate
staleTimeper query - Minimize bundle size (check with visualizer)
- Enable compression (gzip/brotli on server)
Accessibility (a11y)
Semantic HTML
✅ Use semantic elements:
// Good
<nav><a href="/about">About</a></nav>
<button onClick={handleClick}>Submit</button>
<main><article>Content</article></main>
// Bad
<div onClick={handleNav}>About</div>
<div onClick={handleClick}>Submit</div>
<div><div>Content</div></div>
ARIA When Needed
Only add ARIA when semantic HTML isn't enough:
// Custom select component
<div
role="listbox"
aria-label="Select country"
aria-activedescendant={activeId}
>
<div role="option" id="us">United States</div>
<div role="option" id="uk">United Kingdom</div>
</div>
// Loading state
<button aria-busy={isLoading} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Submit'}
</button>
Keyboard Navigation
Ensure all interactive elements are keyboard accessible:
function Dialog({ isOpen, onClose }: DialogProps) {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
if (isOpen) {
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}
}, [isOpen, onClose])
return isOpen ? (
<div role="dialog" aria-modal="true">
{/* Focus trap implementation */}
<button onClick={onClose} aria-label="Close dialog">×</button>
{/* Dialog content */}
</div>
) : null
}
Testing with React Testing Library
Use accessible queries (by role/label):
import { render, screen } from '@testing-library/react'
test('button is accessible', () => {
render(<button>Submit</button>)
// ✅ Good - query by role
const button = screen.getByRole('button', { name: /submit/i })
expect(button).toBeInTheDocument()
// ❌ Avoid - query by test ID
const button = screen.getByTestId('submit-button')
})
Common accessible queries:
// By role (preferred)
screen.getByRole('button', { name: /submit/i })
screen.getByRole('textbox', { name: /email/i })
screen.getByRole('heading', { level: 1 })
// By label
screen.getByLabelText(/email address/i)
// By text
screen.getByText(/welcome/i)
Color Contrast
- Ensure 4.5:1 contrast ratio for normal text
- Ensure 3:1 contrast ratio for large text (18pt+)
- Don't rely on color alone for meaning
- Test with browser DevTools accessibility panel
Accessibility Checklist
- Use semantic HTML elements
- Add alt text to all images
- Ensure keyboard navigation works
- Provide focus indicators
- Test with screen reader (NVDA/JAWS/VoiceOver)
- Verify color contrast meets WCAG AA
- Use React Testing Library accessible queries
- Add skip links for main content
- Ensure form inputs have labels
Security
Never Ship Secrets
❌ Wrong - secrets in code:
const API_KEY = 'sk_live_abc123' // Exposed in bundle!
✅ Correct - environment variables:
// Only VITE_* variables are exposed to client
const API_KEY = import.meta.env.VITE_PUBLIC_KEY
In .env.local (not committed):
VITE_PUBLIC_KEY=pk_live_abc123 # Public key only!
Backend handles secrets:
// Frontend calls backend, backend uses secret API key
await apiClient.post('/process-payment', { amount, token })
// Backend has access to SECRET_KEY via server env
Validate All Untrusted Data
At boundaries (API responses):
import { z } from 'zod'
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
async function fetchUser(id: string) {
const response = await apiClient.get(`/users/${id}`)
// Validate response
return UserSchema.parse(response.data)
}
User input:
const formSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be 8+ characters'),
})
type FormData = z.infer<typeof formSchema>
function LoginForm() {
const handleSubmit = (data: unknown) => {
const result = formSchema.safeParse(data)
if (!result.success) {
setErrors(result.error.errors)
return
}
// result.data is typed and validated
login(result.data)
}
}
XSS Prevention
React automatically escapes content in JSX:
// ✅ Safe - React escapes
<div>{userInput}</div>
// ❌ Dangerous - bypasses escaping
<div dangerouslySetInnerHTML={{ __html: userInput }} />
If you must use HTML:
import DOMPurify from 'dompurify'
<div dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(trustedHTML)
}} />
Content Security Policy
Add CSP headers on server:
# nginx example
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.example.com;
";
Dependency Security
Pin versions in package.json:
{
"dependencies": {
"react": "19.0.0", // Exact version
"@tanstack/react-query": "^5.59.0" // Allow patches
}
}
Audit regularly:
pnpm audit
pnpm audit --fix
Use Renovate or Dependabot:
// .github/renovate.json
{
"extends": ["config:base"],
"automerge": true,
"major": { "automerge": false }
}
CI Security
Run with --ignore-scripts:
# Prevents malicious post-install scripts
pnpm install --ignore-scripts
Scan for secrets:
# Add to CI
git-secrets --scan
Security Checklist
- Never commit secrets or API keys
- Only expose
VITE_*env vars to client - Validate all API responses with Zod
- Sanitize user-generated HTML (if needed)
- Set Content Security Policy headers
- Pin dependency versions
- Run
pnpm auditregularly - Enable Renovate/Dependabot
- Use
--ignore-scriptsin CI - Implement proper authentication flow
Related Skills
- core-principles - Project structure and standards
- react-patterns - Compiler-friendly code
- tanstack-query - Performance via caching and deduplication
- tooling-setup - TypeScript strict mode for type safety