| 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
- 60 fps target — 16.67 ms per frame max in game rendering
- Minimize re-renders —
React.memo, useMemo, useCallback used deliberately after profiling
- Refs for 60 Hz mutations — never
useState for per-frame updates
- Batch draw calls — group similar geometries/materials in Three.js
- InstancedMesh for > 10 similar objects (particles, enemies, bullets)
- Optimize geometry — low polycounts; LOD for distant objects; frustum culling
- Lazy-load assets — textures/models/sounds on demand; preload only critical
- Code-split — dynamic
import() by route/feature; React.lazy + Suspense
- Memoize expensive calculations —
useMemo with stable deps
- Debounce/throttle resize, scroll, input handlers
- Profile first — React DevTools Profiler + Chrome Performance + Spector.js before optimizing
- Bundle budget — < 500 KB gzipped initial load (matches
budget.json)
- Tree-shake — import individual symbols, not whole modules
- Audio: reuse Howler instances; unload unused sounds
- 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
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 (
<div className="hud">
<span>Score: {score}</span>
<span style={{ color: healthColor }}>Health: {health}%</span>
<button onClick={handlePause}>Pause</button>
</div>
);
}
export const GameHUD = memo(GameHUDInner);
✅ Ref-based animation — zero re-renders
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
function Spinner(): JSX.Element {
const mesh = useRef<THREE.Mesh>(null);
useFrame((_, delta) => {
if (mesh.current) mesh.current.rotation.y += delta;
});
return <mesh ref={mesh}><boxGeometry /><meshStandardMaterial /></mesh>;
}
✅ Stable references outside render
const ORIGIN = [0, 0, 0] as const;
function At(): JSX.Element {
return <mesh position={ORIGIN}><sphereGeometry /></mesh>;
}
✅ Code-splitting a heavy overlay
import { lazy, Suspense } from 'react';
const HelpOverlay = lazy(() => import('./HelpOverlay'));
function App(): JSX.Element {
return (
<Suspense fallback={null}>
<HelpOverlay />
</Suspense>
);
}
❌ Anti-Patterns
useFrame(() => setRotation(r => r + 0.01));
<mesh position={[0, 0, 0]}>…</mesh>
const sorted = data.sort((a, b) => a - b);
import _ from 'lodash';
Profiling Workflow
- Measure before — React Profiler (component renders), Performance panel (frame times), Spector.js (draw calls)
- Identify the hottest component or the longest task
- Hypothesize (re-render? layout thrash? draw calls? allocation?)
- Change one thing and re-measure
- 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