Performance Optimization Skill
Context
This skill applies when:
- •Building features that may impact frame rate or rendering performance
- •Optimizing React re-renders in game components
- •Reducing Three.js draw calls and GPU overhead
- •Minimizing bundle size and improving load times
- •Profiling performance bottlenecks
- •Implementing animations and game loops
- •Handling large datasets or complex 3D scenes
Rules
- •Target 60 FPS: Maintain consistent 60 frames per second (16.67ms per frame) in game rendering
- •Minimize Re-renders: Use React.memo, useMemo, and useCallback to prevent unnecessary component updates
- •Use Refs for Mutations: Never use useState for high-frequency updates (60fps) - use refs instead
- •Batch Draw Calls: Group similar geometries and materials to reduce Three.js draw calls
- •Instance Repeated Objects: Use InstancedMesh for multiple similar objects (particles, enemies, bullets)
- •Optimize Geometry: Use lower polygon counts and Level of Detail (LOD) for distant objects
- •Lazy Load Assets: Load textures, models, and sounds on-demand, not at startup
- •Code Splitting: Use dynamic imports to split bundle by route and feature
- •Memoize Expensive Computations: Cache results of complex calculations with useMemo
- •Debounce/Throttle Events: Limit frequency of event handlers (resize, scroll, input)
- •Profile Before Optimizing: Use React DevTools Profiler and Chrome DevTools before making changes
- •Monitor Bundle Size: Keep bundle size under 500KB (gzipped) for initial load
- •Use Web Workers: Offload heavy computations (physics, AI) to background threads
- •Optimize Images: Compress textures and use appropriate formats (WebP, basis)
- •Tree Shake Dependencies: Import only what you need from libraries
Examples
✅ Good Pattern: Memoized Component
typescript
import { memo, useMemo, useCallback } from 'react';
import type { FC } from 'react';
interface GameHUDProps {
score: number;
health: number;
ammo: number;
onPause: () => void;
}
/**
* Memoized HUD component - only re-renders when props change
* Prevents expensive re-renders during 60fps game loop
*/
export const GameHUD: FC<GameHUDProps> = memo(({
score,
health,
ammo,
onPause
}) => {
// Memoize expensive color calculation
const healthColor = useMemo(() => {
if (health > 75) return '#00ff00';
if (health > 25) return '#ffff00';
return '#ff0000';
}, [health]);
// Memoize event handler to prevent child re-renders
const handlePause = useCallback(() => {
onPause();
}, [onPause]);
return (
<div className="game-hud">
<div className="score">Score: {score}</div>
<div className="health" style={{ color: healthColor }}>
Health: {health}%
</div>
<div className="ammo">Ammo: {ammo}</div>
<button onClick={handlePause}>Pause</button>
</div>
);
});
GameHUD.displayName = 'GameHUD';
✅ Good Pattern: Refs for High-Frequency Updates
typescript
import { useRef, useCallback } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
/**
* Player component optimized for 60fps updates
* Uses refs instead of state for position/velocity to avoid re-renders
*/
export function OptimizedPlayer(): JSX.Element {
const meshRef = useRef<THREE.Mesh>(null);
// Store velocity in ref - no re-renders on update
const velocityRef = useRef(new THREE.Vector3(0, 0, 0));
const keysPressed = useRef<Set<string>>(new Set());
// Handle keyboard input without re-renders
const handleKeyDown = useCallback((e: KeyboardEvent) => {
keysPressed.current.add(e.key);
}, []);
const handleKeyUp = useCallback((e: KeyboardEvent) => {
keysPressed.current.delete(e.key);
}, []);
// Game loop - runs 60fps without triggering React re-renders
useFrame((state, delta) => {
if (!meshRef.current) return;
const speed = 5;
const velocity = velocityRef.current;
// Update velocity based on input
velocity.set(0, 0, 0);
if (keysPressed.current.has('w')) velocity.z -= speed * delta;
if (keysPressed.current.has('s')) velocity.z += speed * delta;
if (keysPressed.current.has('a')) velocity.x -= speed * delta;
if (keysPressed.current.has('d')) velocity.x += speed * delta;
// Update position directly on Three.js object
meshRef.current.position.add(velocity);
});
return (
<mesh ref={meshRef}>
<sphereGeometry args={[0.5, 32, 32]} />
<meshStandardMaterial color="#00ff00" />
</mesh>
);
}
✅ Good Pattern: Instanced Mesh for Performance
typescript
import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
interface BulletSystemProps {
maxBullets: number;
}
/**
* Optimized bullet system using instancing
* Renders 1000+ bullets with single draw call
*/
export function BulletSystem({ maxBullets }: BulletSystemProps): JSX.Element {
const meshRef = useRef<THREE.InstancedMesh>(null);
// Pre-allocate bullet data (no allocation in game loop)
const bullets = useMemo(() => {
return Array.from({ length: maxBullets }, () => ({
active: false,
position: new THREE.Vector3(),
velocity: new THREE.Vector3(),
lifetime: 0
}));
}, [maxBullets]);
// Reusable objects to avoid GC pressure
const matrix = useMemo(() => new THREE.Matrix4(), []);
const tempObject = useMemo(() => new THREE.Object3D(), []);
useFrame((state, delta) => {
if (!meshRef.current) return;
let activeCount = 0;
bullets.forEach((bullet, i) => {
if (!bullet.active) return;
// Update bullet position
bullet.position.addScaledVector(bullet.velocity, delta);
bullet.lifetime -= delta;
// Deactivate if expired
if (bullet.lifetime <= 0) {
bullet.active = false;
return;
}
// Update instance matrix
tempObject.position.copy(bullet.position);
tempObject.updateMatrix();
meshRef.current!.setMatrixAt(activeCount, tempObject.matrix);
activeCount++;
});
// Set visible instance count
meshRef.current.count = activeCount;
meshRef.current.instanceMatrix.needsUpdate = true;
});
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, maxBullets]}>
<sphereGeometry args={[0.1, 8, 8]} />
<meshBasicMaterial color="#ffff00" />
</instancedMesh>
);
}
✅ Good Pattern: Code Splitting
typescript
import { lazy, Suspense } from 'react';
import { Canvas } from '@react-three/fiber';
// Lazy load heavy game components
const GameWorld = lazy(() => import('./GameWorld'));
const GameUI = lazy(() => import('./GameUI'));
const GameAudio = lazy(() => import('./GameAudio'));
/**
* Main game component with code splitting
* Loads components on-demand to reduce initial bundle size
*/
export function Game(): JSX.Element {
return (
<div className="game-container">
<Suspense fallback={<LoadingScreen />}>
<Canvas>
<GameWorld />
</Canvas>
<GameUI />
<GameAudio />
</Suspense>
</div>
);
}
function LoadingScreen(): JSX.Element {
return (
<div className="loading-screen">
<div className="spinner" />
<p>Loading game...</p>
</div>
);
}
✅ Good Pattern: Debounced Event Handlers
typescript
import { useCallback, useRef } from 'react';
/**
* Custom hook for debouncing expensive operations
* Prevents performance issues from rapid event firing
*/
export function useDebounce<T extends (...args: any[]) => any>(
callback: T,
delay: number
): (...args: Parameters<T>) => void {
const timeoutRef = useRef<NodeJS.Timeout>();
return useCallback((...args: Parameters<T>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
}
// Usage in component
export function GameSettings(): JSX.Element {
const saveSettings = useCallback((settings: GameSettings) => {
// Expensive operation
localStorage.setItem('settings', JSON.stringify(settings));
}, []);
// Debounce saves to avoid excessive localStorage writes
const debouncedSave = useDebounce(saveSettings, 500);
const handleVolumeChange = (volume: number): void => {
debouncedSave({ volume });
};
return (
<input
type="range"
onChange={(e) => handleVolumeChange(Number(e.target.value))}
/>
);
}
✅ Good Pattern: Web Worker for Heavy Computation
typescript
// pathfinding.worker.ts
self.addEventListener('message', (e: MessageEvent) => {
const { start, end, obstacles } = e.data;
// Heavy A* pathfinding computation
const path = calculatePath(start, end, obstacles);
self.postMessage({ path });
});
// Game component
import { useCallback, useEffect, useRef } from 'react';
export function usePathfinding() {
const workerRef = useRef<Worker>();
useEffect(() => {
// Create worker on mount
workerRef.current = new Worker(
new URL('./pathfinding.worker.ts', import.meta.url),
{ type: 'module' }
);
return () => {
workerRef.current?.terminate();
};
}, []);
const findPath = useCallback((
start: Vector3,
end: Vector3,
obstacles: Vector3[]
): Promise<Vector3[]> => {
return new Promise((resolve) => {
if (!workerRef.current) {
resolve([]);
return;
}
const handler = (e: MessageEvent) => {
workerRef.current?.removeEventListener('message', handler);
resolve(e.data.path);
};
workerRef.current.addEventListener('message', handler);
workerRef.current.postMessage({ start, end, obstacles });
});
}, []);
return { findPath };
}
❌ Bad Pattern: Unnecessary Re-renders
typescript
// Bad: Component re-renders every frame
function BadGameHUD({ gameState }: { gameState: GameState }) {
// Expensive computation on every render
const healthColor = gameState.health > 50 ? '#00ff00' : '#ff0000';
return (
<div>
<div style={{ color: healthColor }}>
Health: {gameState.health}
</div>
</div>
);
}
// This HUD will re-render 60 times per second if parent updates!
❌ Bad Pattern: useState for High-Frequency Updates
typescript
// Bad: Triggers 60 re-renders per second
function BadPlayer() {
const [position, setPosition] = useState({ x: 0, y: 0, z: 0 });
useFrame((state, delta) => {
// DON'T DO THIS - causes 60 re-renders/second!
setPosition(prev => ({
x: prev.x + delta,
y: prev.y,
z: prev.z
}));
});
return <mesh position={[position.x, position.y, position.z]} />;
}
❌ Bad Pattern: Not Using Instancing
typescript
// Bad: 1000 individual meshes = 1000 draw calls
function BadBullets({ bullets }: { bullets: Bullet[] }) {
return (
<>
{bullets.map((bullet, i) => (
<mesh key={i} position={bullet.position}>
<sphereGeometry args={[0.1]} />
<meshBasicMaterial color="#ffff00" />
</mesh>
))}
</>
);
}
❌ Bad Pattern: Memory Allocation in Game Loop
typescript
// Bad: Creates new objects every frame (60 fps = GC pressure)
useFrame((state, delta) => {
bullets.forEach(bullet => {
// DON'T: Creating new Vector3 every frame!
const velocity = new THREE.Vector3(0, 0, -1);
const scaledVelocity = velocity.multiplyScalar(delta);
bullet.position.add(scaledVelocity);
});
});
// Good: Reuse objects
const tempVelocity = useMemo(() => new THREE.Vector3(), []);
useFrame((state, delta) => {
bullets.forEach(bullet => {
tempVelocity.set(0, 0, -1).multiplyScalar(delta);
bullet.position.add(tempVelocity);
});
});
❌ Bad Pattern: Large Bundle Without Splitting
typescript
// Bad: Importing everything upfront
import { GameWorld } from './GameWorld';
import { GameUI } from './GameUI';
import { GameAudio } from './GameAudio';
import { GamePhysics } from './GamePhysics';
import { GameAI } from './GameAI';
// ... 50 more imports
// Result: 5MB initial bundle, slow load time
References
Performance Tools
- •React DevTools Profiler
- •Chrome DevTools Performance
- •Lighthouse
- •Webpack Bundle Analyzer
- •Three.js Stats
Best Practices
Optimization Guides
Remember
- •60 FPS is Critical: Frame time must stay under 16.67ms for smooth gameplay
- •Profile First: Use DevTools Profiler before optimizing - measure, don't guess
- •Refs for Game Loop: Never use useState in useFrame - use refs for mutations
- •Memoize Everything: Use memo, useMemo, useCallback to prevent re-renders
- •Instance Repeated Objects: InstancedMesh is 10-100x faster for many similar objects
- •Batch Draw Calls: Group similar materials/geometries to reduce GPU overhead
- •Code Split: Lazy load routes and features to reduce initial bundle size
- •Avoid Allocations: Reuse objects in game loop to reduce garbage collection pressure
- •Web Workers: Offload heavy computations (pathfinding, physics) to background threads
- •Optimize Assets: Compress textures, use LOD, lazy load resources
- •Debounce Events: Throttle rapid events (resize, input) to prevent performance spikes
- •Monitor Bundle Size: Keep initial bundle under 500KB gzipped
- •Test on Low-End Devices: Profile on minimum spec hardware, not just development machines
- •React Profiler: Use React DevTools Profiler to identify slow components
- •Three.js Stats: Monitor FPS, draw calls, and memory in development
- •Lighthouse Scores: Aim for 90+ Performance score in Lighthouse audits