| name | webgl-performance-optimizer |
| description | Optimize Three.js and WebGL performance, reduce GPU load, suggest fallback UI strategies, and maintain 60fps experience. |
WebGL Performance Optimizer
Core Philosophy
WebGL performance optimization is about intelligent resource management - knowing when to render, what to render, and how to render efficiently. Every frame has a budget of ~16.67ms to maintain 60fps. Your job is to ensure the GPU never exceeds this budget through smart geometry, texture, and draw call management.
Key Principles:
- 🎯 Profile First: Measure before optimizing - identify actual bottlenecks
- 🔄 Progressive Degradation: Gracefully reduce quality when performance drops
- 📊 Frame Budget: Target 60fps desktop (16.67ms/frame), 30fps mobile (33.33ms/frame)
- 🎨 Visual Fidelity Trade-offs: Balance beauty with performance
- 🛡️ Fallback Strategies: Always have non-WebGL alternatives ready
Technology Stack
- Three.js: Core 3D rendering library
- React Three Fiber (@react-three/fiber): React renderer for Three.js
- React Three Drei (@react-three/drei): Useful helpers and abstractions
- Stats.js: Real-time performance monitoring
- Chrome DevTools: GPU profiling and frame analysis
- Spector.js: WebGL debugging and call inspection
When to Use This Skill
Use webgl-performance-optimizer when:
- Three.js scenes drop below 60fps on desktop or 30fps on mobile
- GPU usage is high (>80%) during normal operation
- Complex 3D UI elements cause jank or stuttering
- Users on lower-end devices report performance issues
- Draw calls exceed 500-1000 per frame
- Memory usage grows unbounded over time
- You need to balance visual quality with performance
Optimization Workflow
1. Identify Bottlenecks
import Stats from 'three/examples/jsm/libs/stats.module'
const StatsMonitor = () => {
const statsRef = useRef<Stats>()
useEffect(() => {
const stats = new Stats()
stats.showPanel(0)
stats.dom.style.position = 'absolute'
stats.dom.style.top = '0'
stats.dom.style.left = '0'
document.body.appendChild(stats.dom)
statsRef.current = stats
return () => document.body.removeChild(stats.dom)
}, [])
useFrame(() => {
statsRef.current?.update()
})
return null
}
<Canvas>
<StatsMonitor />
{}
</Canvas>
Chrome DevTools GPU Profiling:
- Open DevTools → Performance → Enable "Screenshots" and "Memory"
- Record a session while interacting with 3D UI
- Check for long frames (>16.67ms) in the flame chart
- Identify draw calls, shader compilation, texture uploads
2. Reduce Draw Calls
Problem: Each mesh = 1 draw call. Too many objects = GPU bottleneck.
Solution 1: Instanced Rendering
{particles.map((pos, i) => (
<mesh key={i} position={pos}>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="cyan" />
</mesh>
))}
import { Instances, Instance } from '@react-three/drei'
<Instances limit={1000}>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="cyan" />
{particles.map((pos, i) => (
<Instance key={i} position={pos} />
))}
</Instances>
Solution 2: Geometry Merging
import { mergeBufferGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils'
const MergedStaticObjects = () => {
const mergedGeometry = useMemo(() => {
const geometries = staticObjects.map(obj => {
const geo = new BoxGeometry(obj.size, obj.size, obj.size)
geo.translate(obj.position.x, obj.position.y, obj.position.z)
return geo
})
return mergeBufferGeometries(geometries)
}, [staticObjects])
return (
<mesh geometry={mergedGeometry}>
<meshStandardMaterial />
</mesh>
)
}
3. Optimize Geometry
Reduce Polygon Count:
<sphereGeometry args={[1, 64, 64]} />
<sphereGeometry args={[1, 16, 16]} />
const DetailedSphere = ({ radius, screenSize }) => {
const segments = screenSize > 1920 ? 32 : screenSize > 1280 ? 24 : 16
return <sphereGeometry args={[radius, segments, segments]} />
}
Use LOD (Level of Detail):
import { LOD } from 'three'
const AdaptiveMesh = ({ position }) => {
const lodRef = useRef<LOD>()
useEffect(() => {
const lod = new LOD()
const highGeo = new SphereGeometry(1, 32, 32)
const highMesh = new Mesh(highGeo, material)
lod.addLevel(highMesh, 0)
const medGeo = new SphereGeometry(1, 16, 16)
const medMesh = new Mesh(medGeo, material)
lod.addLevel(medMesh, 10)
const lowGeo = new SphereGeometry(1, 8, 8)
const lowMesh = new Mesh(lowGeo, material)
lod.addLevel(lowMesh, 20)
lodRef.current = lod
}, [])
return <primitive object={lodRef.current} position={position} />
}
4. Optimize Textures
Texture Compression and Sizing:
import { useTexture } from '@react-three/drei'
const texture = useTexture('/texture-4k.png')
const OptimizedTexture = () => {
const texture = useTexture('/texture-1k-compressed.jpg')
texture.generateMipmaps = true
texture.minFilter = THREE.LinearMipmapLinearFilter
texture.magFilter = THREE.LinearFilter
texture.anisotropy = 2
return <meshStandardMaterial map={texture} />
}
Texture Atlasing:
materials.map(mat => <meshBasicMaterial map={mat.texture} />)
const AtlasMaterial = ({ uvOffset, uvScale }) => {
const atlas = useTexture('/texture-atlas.jpg')
return (
<meshBasicMaterial
map={atlas}
map-offset={uvOffset}
map-repeat={uvScale}
/>
)
}
5. Optimize Shaders
Avoid Expensive Operations:
// Bad: Expensive operations in fragment shader
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
// Normalize in fragment shader (runs per-pixel)
vec3 normal = normalize(vNormal);
// Expensive calculations per pixel
float pattern = sin(vPosition.x * 100.0) * cos(vPosition.y * 100.0);
gl_FragColor = vec4(normal * pattern, 1.0);
}
// Good: Move calculations to vertex shader (runs per-vertex)
// Vertex Shader
attribute vec3 position;
attribute vec3 normal;
varying vec3 vNormal;
varying float vPattern;
void main() {
vNormal = normalize(normalMatrix * normal); // Normalize once per vertex
vPattern = sin(position.x * 100.0) * cos(position.y * 100.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// Fragment Shader
varying vec3 vNormal;
varying float vPattern;
void main() {
gl_FragColor = vec4(vNormal * vPattern, 1.0);
}
Shader Precision:
// Use appropriate precision for mobile
precision mediump float; // Good for mobile (saves GPU power)
precision highp float; // Use only when necessary
// Mix precisions for optimal performance
varying lowp vec4 vColor; // Color doesn't need high precision
varying mediump vec2 vUv; // UVs need medium precision
varying highp vec3 vPosition; // Positions may need high precision
6. Frustum Culling & Occlusion
import { useThree } from '@react-three/fiber'
const FrustumCulledObject = ({ position, children }) => {
const { camera } = useThree()
const meshRef = useRef()
const [visible, setVisible] = useState(true)
useFrame(() => {
if (!meshRef.current) return
const frustum = new THREE.Frustum()
const matrix = new THREE.Matrix4().multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
)
frustum.setFromProjectionMatrix(matrix)
const isVisible = frustum.intersectsObject(meshRef.current)
setVisible(isVisible)
})
return visible ? (
<group ref={meshRef} position={position}>
{children}
</group>
) : null
}
7. Conditional Rendering
const PerformanceAdaptiveScene = () => {
const [quality, setQuality] = useState<'high' | 'medium' | 'low'>('high')
const fpsRef = useRef(60)
useFrame((state, delta) => {
const currentFps = 1 / delta
fpsRef.current = fpsRef.current * 0.9 + currentFps * 0.1
if (fpsRef.current < 30 && quality !== 'low') {
setQuality('low')
} else if (fpsRef.current < 50 && quality === 'high') {
setQuality('medium')
} else if (fpsRef.current > 55 && quality !== 'high') {
setQuality('high')
}
})
return (
<>
{quality === 'high' && <HighQualityEffects />}
{quality === 'medium' && <MediumQualityEffects />}
{quality === 'low' && <MinimalEffects />}
<Instances limit={quality === 'high' ? 1000 : quality === 'medium' ? 500 : 200}>
{/* Adaptive particle count */}
</Instances>
</>
)
}
8. Memory Management
const ManagedMesh = () => {
const meshRef = useRef<THREE.Mesh>()
useEffect(() => {
return () => {
if (meshRef.current) {
meshRef.current.geometry.dispose()
if (Array.isArray(meshRef.current.material)) {
meshRef.current.material.forEach(mat => mat.dispose())
} else {
meshRef.current.material.dispose()
}
}
}
}, [])
return <mesh ref={meshRef}>{/* ... */}</mesh>
}
const textureRef = useRef<THREE.Texture>()
useEffect(() => {
return () => {
textureRef.current?.dispose()
}
}, [])
Fallback Strategies
1. WebGL Detection & Fallback
const WebGLDetector = () => {
const [webglAvailable, setWebglAvailable] = useState(true)
useEffect(() => {
const canvas = document.createElement('canvas')
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
setWebglAvailable(!!gl)
}, [])
if (!webglAvailable) {
return (
<div className="fallback-ui">
<h2>3D View Unavailable</h2>
<p>Your browser doesn't support WebGL. Here's a 2D alternative:</p>
<StaticImageFallback />
</div>
)
}
return <Canvas>{/* 3D Scene */}</Canvas>
}
2. Progressive Enhancement
const AdaptiveVisualization = () => {
const [mode, setMode] = useState<'3d' | '2d' | 'minimal'>('3d')
const performanceRef = useRef({ fps: 60, memory: 0 })
useEffect(() => {
const checkPerformance = () => {
const { fps, memory } = performanceRef.current
if (fps < 20 || memory > 500) {
setMode('minimal')
} else if (fps < 40 || memory > 300) {
setMode('2d')
}
}
const interval = setInterval(checkPerformance, 5000)
return () => clearInterval(interval)
}, [])
return (
<>
{mode === '3d' && <Full3DScene />}
{mode === '2d' && <Simplified2DView />}
{mode === 'minimal' && <StaticFallback />}
</>
)
}
3. User-Controlled Quality
const QualitySettings = () => {
const [userQuality, setUserQuality] = useState<'auto' | 'high' | 'low'>('auto')
return (
<div className="quality-controls">
<select value={userQuality} onChange={e => setUserQuality(e.target.value)}>
<option value="auto">Auto</option>
<option value="high">High Quality</option>
<option value="low">Performance Mode</option>
</select>
<Canvas dpr={userQuality === 'low' ? [1, 1] : [1, 2]}>
<PerformanceOptimizedScene quality={userQuality} />
</Canvas>
</div>
)
}
Performance Targets
Desktop (60fps = 16.67ms/frame)
- Draw Calls: < 500 per frame
- Triangles: < 500k per frame
- Texture Memory: < 500MB total
- Shader Complexity: < 100 instructions per shader
- Particles: < 10k active particles
Mobile (30fps = 33.33ms/frame)
- Draw Calls: < 200 per frame
- Triangles: < 100k per frame
- Texture Memory: < 200MB total
- Shader Complexity: < 50 instructions per shader
- Particles: < 2k active particles
Canvas Setup for Performance
<Canvas
dpr={[1, 2]}
gl={{
antialias: false,
alpha: false,
stencil: false,
depth: true,
powerPreference: 'high-performance',
failIfMajorPerformanceCaveat: true,
}}
frameloop="demand"
camera={{
fov: 50,
near: 0.1,
far: 100,
}}
>
{}
</Canvas>
Quick Wins Checklist
Resources
See /references directory for:
optimization-techniques.md - Advanced optimization methods
fallback-strategies.md - Progressive enhancement approaches
performance-monitoring.md - Profiling and debugging tools
Remember
"Premature optimization is the root of all evil, but so is premature de-optimization." - Profile first, optimize based on data, not assumptions. Always maintain 60fps on desktop and 30fps on mobile as your baseline targets.
When NOT to Use This Skill
- 2D applications without intensive rendering — WebGL optimization is overkill for standard 2D CSS-based UIs; use this skill only when Canvas 2D or CSS performance is genuinely insufficient
- Applications with accessibility as the primary constraint — WebGL content is opaque to assistive technology; prioritize semantic HTML and accessible alternatives before WebGL optimization
- Projects without a performance baseline — never optimize blindly; establish frame-rate and memory measurements before applying WebGL optimizations
Common Mistakes
- Creating new WebGL geometry or shader programs per frame instead of reusing them — per-frame object creation triggers garbage collection and causes frame drops; always pool and reuse WebGL resources
- Not using Frustum Culling — rendering off-screen objects wastes GPU cycles; cull objects outside the camera frustum before issuing draw calls
- Uploading textures to the GPU without mipmaps — textures without mipmaps cause aliasing artifacts and poor sampling performance; always generate mipmaps for textures used at multiple sizes
Related Skills