Skip to content

Latest commit

 

History

History
153 lines (117 loc) · 4.73 KB

File metadata and controls

153 lines (117 loc) · 4.73 KB
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-rendersReact.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 calculationsuseMemo 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

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

// BAD: useState in useFrame — 60 re-renders/sec
useFrame(() => setRotation(r => r + 0.01));

// BAD: new object every render
<mesh position={[0, 0, 0]}></mesh>

// 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)