---
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:**
```typescript
import { lazy, Suspense } from 'react'
// Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'))
function Dashboard() {
return (
}>
)
}
```
**Route-level lazy loading:**
```typescript
// 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:**
```typescript
// Imports are optimized and hashed
import logo from './logo.png'
```
**Prefer modern formats:**
```typescript
// WebP for photos
// SVG for icons
import { ReactComponent as Icon } from './icon.svg'
```
**Lazy load images:**
```typescript
```
**Responsive images:**
```typescript
```
### Bundle Analysis
```bash
# Build with analysis
npx vite build --mode production
# Visualize bundle
pnpm add -D rollup-plugin-visualizer
```
```typescript
// 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 `staleTime` per query
- [ ] Minimize bundle size (check with visualizer)
- [ ] Enable compression (gzip/brotli on server)
## Accessibility (a11y)
### Semantic HTML
**✅ Use semantic elements:**
```typescript
// Good
Content
// Bad
About
Submit
```
### ARIA When Needed
**Only add ARIA when semantic HTML isn't enough:**
```typescript
// Custom select component
United States
United Kingdom
// Loading state
```
### Keyboard Navigation
**Ensure all interactive elements are keyboard accessible:**
```typescript
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 ? (
{/* Focus trap implementation */}
{/* Dialog content */}
) : null
}
```
### Testing with React Testing Library
**Use accessible queries (by role/label):**
```typescript
import { render, screen } from '@testing-library/react'
test('button is accessible', () => {
render()
// ✅ 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:**
```typescript
// 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:**
```typescript
const API_KEY = 'sk_live_abc123' // Exposed in bundle!
```
**✅ Correct - environment variables:**
```typescript
// Only VITE_* variables are exposed to client
const API_KEY = import.meta.env.VITE_PUBLIC_KEY
```
**In `.env.local` (not committed):**
```bash
VITE_PUBLIC_KEY=pk_live_abc123 # Public key only!
```
**Backend handles secrets:**
```typescript
// 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):**
```typescript
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:**
```typescript
const formSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be 8+ characters'),
})
type FormData = z.infer
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:
```typescript
// ✅ Safe - React escapes
{userInput}
// ❌ Dangerous - bypasses escaping
```
**If you must use HTML:**
```typescript
import DOMPurify from 'dompurify'
```
### Content Security Policy
Add CSP headers on server:
```nginx
# 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:**
```json
{
"dependencies": {
"react": "19.0.0", // Exact version
"@tanstack/react-query": "^5.59.0" // Allow patches
}
}
```
**Audit regularly:**
```bash
pnpm audit
pnpm audit --fix
```
**Use Renovate or Dependabot:**
```json
// .github/renovate.json
{
"extends": ["config:base"],
"automerge": true,
"major": { "automerge": false }
}
```
### CI Security
**Run with `--ignore-scripts`:**
```bash
# Prevents malicious post-install scripts
pnpm install --ignore-scripts
```
**Scan for secrets:**
```bash
# 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 audit` regularly
- [ ] Enable Renovate/Dependabot
- [ ] Use `--ignore-scripts` in 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