| name | performance-security |
| description | Use when optimizing performance or reviewing security. Covers code-splitting, React Compiler patterns, asset optimization, a11y testing, and security hardening for React apps. |
| updated | "2026-01-20T00:00:00.000Z" |
| keywords | performance, security, accessibility, a11y, code-splitting, react-compiler, bundle-optimization |
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'
const HeavyChart = lazy(() => import('./HeavyChart'))
function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart data={data} />
</Suspense>
)
}
Route-level lazy loading:
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:
import logo from './logo.png'
<img src={logo} alt="Logo" />
Prefer modern formats:
<img src="/hero.webp" alt="Hero" />
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
npx vite build --mode production
pnpm add -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
react(),
visualizer({ open: true }),
],
})
Performance Checklist
Accessibility (a11y)
Semantic HTML
✅ Use semantic elements:
<nav><a href="/about">About</a></nav>
<button onClick={handleClick}>Submit</button>
<main><article>Content</article></main>
<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:
<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>
<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>)
const button = screen.getByRole('button', { name: /submit/i })
expect(button).toBeInTheDocument()
const button = screen.getByTestId('submit-button')
})
Common accessible queries:
screen.getByRole('button', { name: /submit/i })
screen.getByRole('textbox', { name: /email/i })
screen.getByRole('heading', { level: 1 })
screen.getByLabelText(/email address/i)
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
Security
Never Ship Secrets
❌ Wrong - secrets in code:
const API_KEY = 'sk_live_abc123'
✅ Correct - environment variables:
const API_KEY = import.meta.env.VITE_PUBLIC_KEY
In .env.local (not committed):
VITE_PUBLIC_KEY=pk_live_abc123
Backend handles secrets:
await apiClient.post('/process-payment', { amount, token })
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}`)
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
}
login(result.data)
}
}
XSS Prevention
React automatically escapes content in JSX:
<div>{userInput}</div>
<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",
"@tanstack/react-query": "^5.59.0"
}
}
Audit regularly:
pnpm audit
pnpm audit --fix
Use Renovate or Dependabot:
{
"extends": ["config:base"],
"automerge": true,
"major": { "automerge": false }
}
CI Security
Run with --ignore-scripts:
pnpm install --ignore-scripts
Scan for secrets:
git-secrets --scan
Security Checklist
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