Claude
Skills
Sign in
Back

threejs-graphics-optimizer

Included with Lifetime
$97 forever

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.

Image & Video

What this skill does


# 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**

```javascript
// ❌ Bad: 100 draw calls for 100 cubes
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)
}

// ✅ Good: 1 draw call via InstancedMesh
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:

```javascript
const lod = new THREE.LOD()

// High detail (near camera)
const highDetailGeo = new THREE.IcosahedronGeometry(1, 3) // Many faces
const highDetailMesh = new THREE.Mesh(
  highDetailGeo,
  new THREE.MeshStandardMaterial({ color: 0x00d9ff })
)
lod.addLevel(highDetailMesh, 0) // Distance 0-10

// Medium detail
const medDetailGeo = new THREE.IcosahedronGeometry(1, 1)
const medDetailMesh = new THREE.Mesh(
  medDetailGeo,
  new THREE.MeshBasicMaterial({ color: 0x00d9ff })
)
lod.addLevel(medDetailMesh, 10) // Distance 10-50

// Low detail (far from camera)
const lowDetailGeo = new THREE.IcosahedronGeometry(1, 0)
const lowDetailMesh = new THREE.Mesh(
  lowDetailGeo,
  new THREE.MeshBasicMaterial({ color: 0x00d9ff })
)
lod.addLevel(lowDetailMesh, 50) // Distance 50+

scene.add(lod)

// Update LOD in render loop
function animate() {
  lod.update(camera)
  renderer.render(scene, camera)
}
```

### 3. Frustum Culling (Automatic)

THREE.js automatically skips objects outside camera view. Help it:

```javascript
// ❌ Bad: Unnecessarily large bounding volumes
mesh.geometry.computeBoundingSphere()
mesh.geometry.boundingSphere.radius = 1000 // Too large!

// ✅ Good: Accurate bounding volumes
mesh.geometry.computeBoundingSphere() // Uses actual geometry size
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)

```javascript
const textureLoader = new THREE.TextureLoader()

// ❌ Bad: Loading 4K texture for small object
const texture = textureLoader.load('texture-4k.jpg')

// ✅ Good: Appropriate size for use case
const texture = textureLoader.load('texture-1k.jpg')

// ✅ Better: Set appropriate filtering
texture.minFilter = THREE.LinearFilter // No mipmaps (saves VRAM)
texture.anisotropy = renderer.capabilities.getMaxAnisotropy()

// ✅ Best: Dispose when done
function cleanup() {
  texture.dispose()
}
```

---

## Part 2: Mobile-Specific Optimization

### Mobile Detection & Adaptation

```javascript
/**
 * Detect mobile device.
 * @returns {boolean}
 */
export function isMobile() {
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile/i.test(navigator.userAgent)
    || window.innerWidth < 768
}

/**
 * Get optimal pixel ratio for device.
 * @returns {number}
 */
export function getOptimalPixelRatio() {
  const mobile = isMobile()
  const deviceRatio = window.devicePixelRatio
  
  // Cap pixel ratio on mobile to save performance
  return mobile 
    ? Math.min(deviceRatio, 1.5)  // Max 1.5x on mobile
    : Math.min(deviceRatio, 2)    // Max 2x on desktop
}

// Apply to renderer
renderer.setPixelRatio(getOptimalPixelRatio())
```

### Mobile Performance Settings

```javascript
/**
 * Configure renderer for mobile performance.
 */
function setupMobileOptimizations(renderer, scene, camera) {
  const mobile = isMobile()
  
  if (mobile) {
    // Disable expensive features
    renderer.shadowMap.enabled = false
    renderer.antialias = false
    
    // Lower pixel ratio
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5))
    
    // Simpler tone mapping
    renderer.toneMapping = THREE.NoToneMapping
    
    // Remove fog (expensive pixel shader)
    scene.fog = null
    
    // Reduce lights (expensive)
    // Keep only 1-2 lights max on mobile
    
    console.log('[Mobile] Performance optimizations applied')
  } else {
    // Desktop: enable high-quality features
    renderer.shadowMap.enabled = true
    renderer.shadowMap.type = THREE.PCFSoftShadowMap
    renderer.antialias = true
    renderer.toneMapping = THREE.ACESFilmicToneMapping
    
    console.log('[Desktop] High-quality features enabled')
  }
}
```

### Fallback Pattern

```javascript
/**
 * Create geometry with fallback for low-end devices.
 */
export function createOptimizedGeometry(options = {}) {
  const { size = 1, mobile = false } = options
  
  if (mobile) {
    // Simple geometry for mobile
    return new THREE.SphereGeometry(size, 8, 8) // Low poly
  } else {
    // Detailed geometry for desktop
    return new THREE.IcosahedronGeometry(size, 2) // High poly
  }
}

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

```javascript
class SceneManager {
  constructor() {
    this.clock = new THREE.Clock()
    this.animationId = null
    this.lastFrameTime = 0
    this.fps = 60
    this.frameInterval = 1000 / this.fps
  }
  
  /**
   * Main render loop with delta time.
   */
  animate() {
    this.animationId = requestAnimationFrame(() => this.animate())
    
    const now = performance.now()
    const delta = now - this.lastFrameTime
    
    // Throttle to target FPS if needed
    if (delta < this.frameInterval) return
    
    this.lastFrameTime = now - (delta % this.frameInterval)
    
    // Update logic with delta
    const deltaSeconds = this.clock.getDelta()
    this.update(deltaSeconds)
    
    // Render
    this.renderer.render(this.scene, this.camera)
  }
  
  /**
   * Update scene objects.
   * @param {number} delta - Time since last frame (seconds)
   */
  update(delta) {
    // Update animations, physics, etc.
    this.animatedObjects.forEach(obj => {
      if (obj.update) obj.update(delta)
    })
  }
  
  /**
   * Cleanup and stop animation.
   */
  dispose() {
    if (this.animationId) {
      cancelAnimationFrame(this.animationId)
    }
  }
}
```

### Conditional Rendering

```javascript
/**
 * Only render when something changed (for static scenes).
 */
class ConditionalRenderer {
  constructor(renderer, scene, camera) {
    this.renderer = renderer
    this.scene = scene
    this.camera = camera
    this.needsRender = true
  }
  
  /**
   * Mark scene as 

Related in Image & Video