--- 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' Logo ``` **Prefer modern formats:** ```typescript // WebP for photos Hero // SVG for icons import { ReactComponent as Icon } from './icon.svg' ``` **Lazy load images:** ```typescript Description ``` **Responsive images:** ```typescript Description ``` ### 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
Content
``` ### 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