| name | threejs-graphics-optimizer |
| description | Performance optimization rules for THREE.js and graphics programming. Covers mobile-first optimization, fallback patterns, memory management, render loop efficiency, and general graphics best practices for smooth 60fps experiences across devices. |
THREE.js Graphics Optimizer
Version: 1.0
Focus: Performance optimization for THREE.js and graphics applications
Purpose: Build smooth 60fps graphics experiences across all devices including mobile
Philosophy: Performance-First Graphics
The 16ms Budget
Target: 60 FPS = 16.67ms per frame
Frame budget breakdown:
- JavaScript logic: ~5-8ms
- Rendering (GPU): ~8-10ms
- Browser overhead: ~2ms
If you exceed 16ms: Frames drop, stuttering occurs.
Mobile vs Desktop Reality
Desktop: Powerful GPU, lots of VRAM, high pixel ratios
Mobile: Constrained GPU, limited VRAM, battery concerns, thermal throttling
Design philosophy: Optimize for mobile, scale up for desktop (not vice versa).
Part 1: Core Optimization Principles
1. Minimize Draw Calls
The Problem: Each object = one draw call. 1000 objects = 1000 calls = slow.
Solution: Geometry Merging
for (let i = 0; i < 100; i++) {
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const cube = new THREE.Mesh(geometry, material)
cube.position.set(i * 2, 0, 0)
scene.add(cube)
}
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const instancedMesh = new THREE.InstancedMesh(geometry, material, 100)
for (let i = 0; i < 100; i++) {
const matrix = new THREE.Matrix4()
matrix.setPosition(i * 2, 0, 0)
instancedMesh.setMatrixAt(i, matrix)
}
instancedMesh.instanceMatrix.needsUpdate = true
scene.add(instancedMesh)
When to use:
- Many similar objects (particles, trees, enemies)
- Static or semi-static positioning
- Shared material/geometry
2. Level of Detail (LOD)
Render simpler geometry when objects are far away:
const lod = new THREE.LOD()
const highDetailGeo = new THREE.IcosahedronGeometry(1, 3)
const highDetailMesh = new THREE.Mesh(
highDetailGeo,
new THREE.MeshStandardMaterial({ color: 0x00d9ff })
)
lod.addLevel(highDetailMesh, 0)
const medDetailGeo = new THREE.IcosahedronGeometry(1, 1)
const medDetailMesh = new THREE.Mesh(
medDetailGeo,
new THREE.MeshBasicMaterial({ color: 0x00d9ff })
)
lod.addLevel(medDetailMesh, 10)
const lowDetailGeo = new THREE.IcosahedronGeometry(1, 0)
lowDetailMesh = .(
lowDetailGeo,
.({ : })
)
lod.(lowDetailMesh, )
scene.(lod)
() {
lod.(camera)
renderer.(scene, camera)
}
3. Frustum Culling (Automatic)
THREE.js automatically skips objects outside camera view. Help it:
mesh.geometry.computeBoundingSphere()
mesh.geometry.boundingSphere.radius = 1000
mesh.geometry.computeBoundingSphere()
mesh.geometry.computeBoundingBox()
4. Texture Optimization
Texture size matters:
- 4K texture (4096x4096): 64MB VRAM (uncompressed)
- 2K texture (2048x2048): 16MB VRAM
- 1K texture (1024x1024): 4MB VRAM
Rules:
- Use smallest textures that look good
- Power-of-two dimensions (512, 1024, 2048)
- Compress textures (use basis/KTX2 format)
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('texture-4k.jpg')
const texture = textureLoader.load('texture-1k.jpg')
texture.minFilter = THREE.LinearFilter
texture.anisotropy = renderer.capabilities.getMaxAnisotropy()
function cleanup() {
texture.dispose()
}
Part 2: Mobile-Specific Optimization
Mobile Detection & Adaptation
export function isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile/i.test(navigator.userAgent)
|| window.innerWidth < 768
}
export function getOptimalPixelRatio() {
const mobile = isMobile()
const deviceRatio = window.devicePixelRatio
return mobile
? Math.min(deviceRatio, 1.5)
: Math.min(deviceRatio, 2)
}
renderer.setPixelRatio(getOptimalPixelRatio())
Mobile Performance Settings
function setupMobileOptimizations(renderer, scene, camera) {
const mobile = isMobile()
if (mobile) {
renderer.shadowMap.enabled = false
renderer.antialias = false
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5))
renderer.toneMapping = THREE.NoToneMapping
scene.fog = null
console.log('[Mobile] Performance optimizations applied')
} else {
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.antialias = true
renderer.toneMapping = THREE.ACESFilmicToneMapping
.()
}
}
Fallback Pattern
export function createOptimizedGeometry(options = {}) {
const { size = 1, mobile = false } = options
if (mobile) {
return new THREE.SphereGeometry(size, 8, 8)
} else {
return new THREE.IcosahedronGeometry(size, 2)
}
}
const mobile = isMobile()
const geometry = createOptimizedGeometry({ size: 1, mobile })
const material = new THREE.MeshBasicMaterial({ color: 0x00d9ff })
const mesh = new THREE.Mesh(geometry, material)
Part 3: Render Loop Optimization
Efficient Animation Loop
class SceneManager {
constructor() {
this.clock = new THREE.Clock()
this.animationId = null
this.lastFrameTime = 0
this.fps = 60
this.frameInterval = 1000 / this.fps
}
animate() {
this.animationId = requestAnimationFrame(() => this.animate())
const now = performance.now()
const delta = now - this.lastFrameTime
if (delta < this.frameInterval) return
this.lastFrameTime = now - (delta % this.frameInterval)
const deltaSeconds = this..()
.(deltaSeconds)
..(., .)
}
() {
..( {
(obj.) obj.(delta)
})
}
() {
(.) {
(.)
}
}
}
Conditional Rendering
class ConditionalRenderer {
constructor(renderer, scene, camera) {
this.renderer = renderer
this.scene = scene
this.camera = camera
this.needsRender = true
}
invalidate() {
this.needsRender = true
}
render() {
if (this.needsRender) {
this.renderer.render(this.scene, this.camera)
this.needsRender = false
}
}
connectControls(controls) {
controls.addEventListener('change', () => this.invalidate())
}
}
const conditionalRenderer = new ConditionalRenderer(renderer, scene, camera)
conditionalRenderer.(controls)
() {
(animate)
controls.()
conditionalRenderer.()
}
Part 4: Memory Management
Dispose Pattern
export function disposeObject(object) {
if (!object) return
object.traverse((child) => {
if (child.geometry) {
child.geometry.dispose()
}
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(material => disposeMaterial(material))
} else {
disposeMaterial(child.material)
}
}
if (child.texture) {
child.texture.dispose()
}
})
if (object.parent) {
object.parent.remove(object)
}
}
function disposeMaterial(material) {
material.dispose()
.(material).( {
value = material[key]
(value && value === && value) {
value.()
}
})
}
Memory Leak Prevention
class SafeSceneManager {
constructor() {
this.scene = new THREE.Scene()
this.renderer = new THREE.WebGLRenderer()
this.objects = new Set()
}
add(object) {
this.scene.add(object)
this.objects.add(object)
}
remove(object) {
this.scene.remove(object)
this.objects.delete(object)
disposeObject(object)
}
dispose() {
this.objects.forEach(obj => disposeObject(obj))
this.objects.()
..()
..()
}
}
Part 5: Material Optimization
Material Sharing
for (let i = 0; i < 100; i++) {
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
}
const sharedMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 })
for (let i = 0; i < 100; i++) {
const mesh = new THREE.Mesh(geometry, sharedMaterial)
scene.add(mesh)
}
Cheaper Material Types
Performance ranking (fastest to slowest):
- MeshBasicMaterial - No lighting, flat shading
- MeshLambertMaterial - Simple diffuse lighting
- MeshPhongMaterial - Specular highlights
- MeshStandardMaterial - PBR (expensive)
- MeshPhysicalMaterial - Advanced PBR (very expensive)
const material = isMobile()
? new THREE.MeshBasicMaterial({ color: 0x00d9ff })
: new THREE.MeshStandardMaterial({
color: 0x00d9ff,
roughness: 0.5,
metalness: 0.1
})
Blending Modes
material.blending = THREE.AdditiveBlending
material.transparent = true
material.depthWrite = false
Part 6: Post-Processing Optimization
Selective Post-Processing
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'
function setupPostProcessing(renderer, scene, camera, mobile) {
const composer = new EffectComposer(renderer)
composer.addPass(new RenderPass(scene, camera))
if (!mobile) {
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5,
0.4,
0.85
)
composer.addPass(bloomPass)
}
return composer
}
Part 7: General Graphics Best Practices
1. Object Pooling
class ObjectPool {
constructor(createFn, resetFn) {
this.pool = []
this.createFn = createFn
this.resetFn = resetFn
}
acquire() {
if (this.pool.length > 0) {
return this.pool.pop()
}
return this.createFn()
}
release(obj) {
this.resetFn(obj)
this.pool.push(obj)
}
}
const particlePool = new ObjectPool(
() => {
const geometry = new THREE.SphereGeometry(0.1)
const material = new THREE.({ : })
.(geometry, material)
},
{
particle..(, , )
particle. =
}
)
particle = particlePool.()
particle..(.(), .(), .())
particle. =
scene.(particle)
scene.(particle)
particlePool.(particle)
2. Visibility Culling
function updateVisibility(camera, objects, maxDistance = 50) {
const cameraPos = camera.position
objects.forEach(obj => {
const distance = obj.position.distanceTo(cameraPos)
obj.visible = distance < maxDistance
})
}
3. Lazy Loading
class LazyTextureLoader {
constructor() {
this.loader = new THREE.TextureLoader()
this.cache = new Map()
}
async load(url) {
if (this.cache.has(url)) {
return this.cache.get(url)
}
return new Promise((resolve, reject) => {
this.loader.load(
url,
(texture) => {
this.cache.set(url, texture)
resolve(texture)
},
undefined,
reject
)
})
}
}
Part 8: Performance Monitoring
FPS Counter
class FPSMonitor {
constructor() {
this.frames = 0
this.lastTime = performance.now()
this.fps = 60
}
update() {
this.frames++
const now = performance.now()
if (now >= this.lastTime + 1000) {
this.fps = Math.round((this.frames * 1000) / (now - this.lastTime))
this.frames = 0
this.lastTime = now
if (this.fps < 30) {
console.warn(`Low FPS: ${this.fps}`)
}
}
}
getFPS() {
return .
}
}
fpsMonitor = ()
() {
(animate)
fpsMonitor.()
renderer.(scene, camera)
}
GPU Memory Monitoring
function logMemoryUsage(renderer) {
const info = renderer.info
console.log('GPU Memory:', {
geometries: info.memory.geometries,
textures: info.memory.textures,
programs: info.programs.length,
drawCalls: info.render.calls,
triangles: info.render.triangles
})
}
setInterval(() => logMemoryUsage(renderer), 5000)
Critical Optimization Checklist
Before Optimizing
Geometry
Textures
Materials
Lighting
Rendering
Mobile-Specific
Common Performance Killers
- Too many draw calls → Use InstancedMesh
- High-resolution textures → Resize to 1K or 2K
- Too many lights → Limit to 2-3
- Transparent objects → Use sparingly, render last
- Post-processing on mobile → Disable or simplify
- Memory leaks → Always dispose geometries/materials/textures
- Unnecessary re-renders → Use conditional rendering
- High pixel ratio on mobile → Cap at 1.5x
Performance Testing Workflow
1. Test on Target Devices
console.log('Device Info:', {
userAgent: navigator.userAgent,
pixelRatio: window.devicePixelRatio,
screen: `${window.screen.width}x${window.screen.height}`,
gpu: renderer.capabilities.getMaxAnisotropy()
})
2. Profile with Chrome DevTools
- Open DevTools → Performance tab
- Record 5-10 seconds of rendering
- Look for:
- Long frames (>16ms)
- GPU bottlenecks
- Memory leaks
3. A/B Test Optimizations
const ENABLE_SHADOWS = !isMobile()
const ENABLE_BLOOM = !isMobile()
const MAX_PARTICLE_COUNT = isMobile() ? 100 : 500
Resources