--- name: performance-optimization description: React re-render optimization, Three.js rendering performance, useMemo/useCallback, bundle size, 60 fps profiling, Lighthouse budgets license: MIT --- # Performance Optimization Skill ## Context Applies when building performance-sensitive features, optimizing re-renders, reducing draw calls, minimizing bundle size, fixing memory leaks, profiling bottlenecks, or implementing game loops. ## Rules 1. **60 fps target** — 16.67 ms per frame max in game rendering 2. **Minimize re-renders** — `React.memo`, `useMemo`, `useCallback` used deliberately after profiling 3. **Refs for 60 Hz mutations** — never `useState` for per-frame updates 4. **Batch draw calls** — group similar geometries/materials in Three.js 5. **InstancedMesh** for > 10 similar objects (particles, enemies, bullets) 6. **Optimize geometry** — low polycounts; LOD for distant objects; frustum culling 7. **Lazy-load assets** — textures/models/sounds on demand; preload only critical 8. **Code-split** — dynamic `import()` by route/feature; `React.lazy` + `Suspense` 9. **Memoize expensive calculations** — `useMemo` with stable deps 10. **Debounce/throttle** resize, scroll, input handlers 11. **Profile first** — React DevTools Profiler + Chrome Performance + Spector.js before optimizing 12. **Bundle budget** — < 500 KB gzipped initial load (matches `budget.json`) 13. **Tree-shake** — import individual symbols, not whole modules 14. **Audio**: reuse Howler instances; unload unused sounds 15. **Memory**: dispose Three.js resources on unmount; break ref cycles ## Lighthouse Budget (aligned with `budget.json`) | Metric | Target | |---|---| | Performance score | ≥ 90 | | LCP | < 2.5 s | | TTI | < 3.5 s | | CLS | < 0.1 | | Initial JS gzipped | < 500 KB | ## Examples ### ✅ Memoized leaf component ```tsx import { memo, useCallback, useMemo } from 'react'; interface HUDProps { score: number; health: number; onPause: () => void; } function GameHUDInner({ score, health, onPause }: HUDProps): JSX.Element { const healthColor = useMemo(() => (health > 50 ? '#0f0' : '#f00'), [health]); const handlePause = useCallback(() => onPause(), [onPause]); return (
Score: {score} Health: {health}%
); } export const GameHUD = memo(GameHUDInner); ``` ### ✅ Ref-based animation — zero re-renders ```tsx import { useRef } from 'react'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; function Spinner(): JSX.Element { const mesh = useRef(null); useFrame((_, delta) => { if (mesh.current) mesh.current.rotation.y += delta; }); return ; } ``` ### ✅ Stable references outside render ```tsx const ORIGIN = [0, 0, 0] as const; function At(): JSX.Element { return ; } ``` ### ✅ Code-splitting a heavy overlay ```tsx import { lazy, Suspense } from 'react'; const HelpOverlay = lazy(() => import('./HelpOverlay')); function App(): JSX.Element { return ( ); } ``` ### ❌ Anti-Patterns ```tsx // BAD: useState in useFrame — 60 re-renders/sec useFrame(() => setRotation(r => r + 0.01)); // BAD: new object every render // BAD: unmemoized expensive calc inside render const sorted = data.sort((a, b) => a - b); // BAD: importing entire lodash import _ from 'lodash'; // BAD: forgetting to dispose geometry / material / texture ``` ## Profiling Workflow 1. **Measure before** — React Profiler (component renders), Performance panel (frame times), Spector.js (draw calls) 2. **Identify** the hottest component or the longest task 3. **Hypothesize** (re-render? layout thrash? draw calls? allocation?) 4. **Change one thing** and re-measure 5. **Record evidence** in the PR (screenshots, numbers, flamegraphs) ## Memory-Leak Patterns - Forgotten `setInterval` / subscriptions → cleanup in `useEffect` return - Undisposed `THREE.Geometry` / `Material` / `Texture` → dispose on unmount - Global event listeners → `removeEventListener` in cleanup - Closures capturing large data → `useRef` or restructure ## Validation Checklist - [ ] Profile shows no regression (or documented acceptable trade-off) - [ ] No `useState` in `useFrame`; 60 Hz updates use refs - [ ] `InstancedMesh` used for > 10 similar objects - [ ] Three.js resources disposed on unmount - [ ] Bundle within budget (`budget.json`) after changes - [ ] Lighthouse score ≥ 90 for changed paths - [ ] No memory leak in 5-minute soak test (DevTools Memory)