Understanding Three.js Scene and Render Lifecycle

Scene Graph Fundamentals

Three.js manages 3D objects in a hierarchical scene graph. Objects like Mesh, Light, and Camera are attached to a Scene and rendered by a WebGLRenderer on each animation frame.

Renderer and Memory Use

Every texture, geometry, and material consumes WebGL GPU memory. If these are not disposed properly when removed from the scene, memory leaks will accumulate—especially in interactive or dynamic scenes (e.g., product viewers, 3D editors).

Symptoms of Performance and Memory Issues

  • Gradual FPS degradation over time (e.g., from 60fps to 20fps)
  • GPU memory growth in developer tools
  • Delayed garbage collection or JavaScript heap warnings
  • Visible stutter during user interactions or animations
  • Chrome's WebGL context lost due to memory exhaustion

Root Causes

1. Undisposed Geometries, Materials, and Textures

Removing objects from the scene without calling dispose() on their resources leaves them in GPU memory, leading to leaks.

2. Event Listeners and Callbacks Not Unbound

Mouse, resize, or animation callbacks tied to old objects can persist beyond object lifetime if not unregistered, causing orphaned closures.

3. Cloning or Replacing Meshes Without Cleanup

When dynamically adding/removing objects, cloned meshes or procedurally generated geometry often accumulate unless explicitly removed and disposed.

4. Renderer Reinitialization in SPA Environments

Recreating WebGLRenderer on every route change or canvas mount/remount leaks GPU contexts unless explicitly released via renderer.dispose().

5. Excessive Draw Calls and Scene Complexity

Large scenes with thousands of separate objects or materials can overwhelm the renderer. Without batching or LOD, render time spikes.

Diagnostics and Analysis

1. Use Chrome's WebGL Profiler

Access chrome://gpu and Chrome DevTools > Performance tab to inspect GPU memory, draw calls, and frame timings.

2. Monitor Object Counts Per Frame

console.log(renderer.info.memory, renderer.info.render);

Reveals how many geometries, textures, and draw calls are active each frame.

3. Use Heap Snapshots

Capture memory snapshots in Chrome to track retained objects. Look for increasing THREE.BufferGeometry or THREE.Texture instances.

4. Profile Event Bindings

Inspect window and DOM elements for redundant listeners via DevTools > Event Listeners tab.

5. Enable WebGL Context Loss Debugging

Track WebGL context lifecycle using WEBGL_lose_context extension or browser logs when memory runs out.

Step-by-Step Fix Strategy

1. Properly Dispose Resources

mesh.geometry.dispose();
mesh.material.dispose();
texture.dispose();

Call dispose on geometry, material, and textures before removing objects from the scene.

2. Remove Objects from the Scene Graph

Use scene.remove(mesh) before disposing resources. This detaches the object and ensures garbage collection.

3. Unbind Event Listeners on Cleanup

Track all addEventListener calls and remove them with removeEventListener when components unmount or objects are destroyed.

4. Reuse Renderer and Scene When Possible

In SPAs, persist the renderer across pages using global state or service pattern to avoid context churn.

5. Reduce Draw Calls and Object Count

Use merged geometries (BufferGeometryUtils.mergeBufferGeometries), instancing (InstancedMesh), and Level of Detail to optimize large scenes.

Best Practices

  • Track all GPU-bound objects and dispose them explicitly
  • Reuse materials and geometries when rendering similar objects
  • Throttle dynamic scene updates with requestAnimationFrame
  • Profile performance during development using DevTools and renderer.info
  • Use texture compression and power-of-two sizes to optimize memory

Conclusion

Three.js makes 3D development on the web approachable, but improper resource handling leads to serious performance degradation. By actively managing scene objects, disposing resources, unbinding callbacks, and profiling with browser tools, developers can keep applications performant and memory-stable—even as scenes grow in complexity. For production-grade 3D apps, understanding and controlling the rendering lifecycle is just as important as visual fidelity.

FAQs

1. Why does my Three.js app slow down over time?

Most likely due to memory leaks—unused geometries, textures, or event listeners still lingering in memory. Dispose resources explicitly.

2. Do I need to call dispose() for every object?

Yes—for GPU-bound resources like geometry, materials, and textures. Otherwise, memory will not be released, even if objects are removed from the scene.

3. Can I use scene.clear() to wipe memory?

scene.clear() removes objects from the graph but doesn’t dispose memory. You must manually call dispose on each object’s components.

4. How do I debug WebGL memory usage?

Use renderer.info in runtime or Chrome's performance profiler to track texture count, draw calls, and retained memory.

5. Is it safe to recreate the renderer each time?

No. Constantly recreating WebGLRenderer can lead to memory exhaustion. Reuse the renderer or dispose it properly with renderer.dispose().