Skip to content

Audit and improve an existing scene

A Three.js guide for coding agents. Also covers audit a three.js scene, audit an r3f app, improve an existing threejs scene, clean up an existing webgl app, my scene gets slower the longer it runs, gpu memory keeps growing while idle, and 8 more.

Show all 14 aliases

audit a three.js scene, audit an r3f app, improve an existing threejs scene, clean up an existing webgl app, my scene gets slower the longer it runs, gpu memory keeps growing while idle, allocation inside useFrame, setState inside useFrame, dispose geometry material texture, frameloop demand invalidate, reduce draw calls in r3f, verify a 3d scene visually, blurry or stretched canvas, debug webgl resource leaks

Use this guide when a Three.js or React Three Fiber scene already works but needs evidence-backed improvements. Treat performance, resource ownership, render correctness, and interaction behavior as separate questions. A source scan can suggest where to look; only a running scene can show whether a change helped.

Establish a repeatable baseline

Choose one route, viewport, device-pixel ratio, camera position, and interaction sequence. Capture these before changing code:

  • a screenshot after the first stable frame
  • frame time while idle and during the main interaction
  • renderer.info.render counts and renderer.info.memory counts
  • a browser performance trace for a slow or stuttering interaction
  • console warnings and WebGL context errors

Record whether the measurement is from development or production. Development instrumentation and React Strict Mode can change timings and lifecycle counts, so compare like with like.

Trace ownership before disposal

Inventory resources by the code that creates and releases them:

ResourceCommon ownerRelease check
Geometry and materialScene component or asset cacheRemoved objects no longer retain them; imperatively created resources call dispose()
TextureLoader cache or owning featureThe owner releases textures that are not shared elsewhere
Render targetPostprocessing pass or custom rendererResize replaces and disposes the old target; unmount disposes the final target
Listener or observerControls, resize logic, pointer integrationCleanup removes the same callback and target that setup registered
Animation loopRenderer or <Canvas>Only one loop owns frame scheduling, and teardown stops it

Do not dispose a shared resource from one consumer while another consumer still uses it. If ownership is unclear, make it explicit before adding cleanup.

For a plain Three.js feature that owns resources directly, keep setup and teardown together:

const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial()
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)

function destroyFeature() {
  scene.remove(mesh)
  geometry.dispose()
  material.dispose()
}

Removing an object from the scene does not release its GPU allocations. Three.js cannot infer whether a texture, material, or geometry will be reused, so the owner must decide when disposal is safe.

Inspect work that repeats

Start with useFrame, requestAnimationFrame, pointer-move handlers, resize handlers, and per-frame postprocessing callbacks. For each one, ask:

  1. Does it allocate objects or arrays?
  2. Does it trigger React state or rebuild scene objects?
  3. Does it traverse more of the scene than the interaction needs?
  4. Does it run while the scene is visually unchanged?

Reuse scratch values in hot paths:

const target = new THREE.Vector3()

function MovingMesh() {
  const mesh = useRef<THREE.Mesh>(null)

  useFrame(({ clock }, delta) => {
    target.set(0, Math.sin(clock.elapsedTime), 0)
    const alpha = 1 - Math.exp(-5 * delta)
    mesh.current?.position.lerp(target, alpha)
  })

  return <mesh ref={mesh} />
}

Use React state for durable application state and discrete events. For values that change every frame, mutate the Three.js object through a ref and use the frame delta when movement should be independent of refresh rate.

Check whether every frame is necessary

An application that changes only after input can use React Three Fiber's demand rendering:

<Canvas frameloop="demand">
  <Scene />
</Canvas>

R3F schedules a frame when React detects prop changes. If code mutates an object outside React, call invalidate() to request a future frame. Do not call invalidate() continuously; that recreates an always-running loop.

When an animation begins after an input event, call invalidate(), then start the animation in the next requestAnimationFrame callback. That gives demand rendering a frame before the animation changes its first value. Confirm controls and animation helpers already invalidate before adding another listener.

Find scene churn and excess draw work

Watch renderer.info while repeating the same action. Counts that rise and never settle point to retained resources or repeated construction. Large stable counts can still indicate excess draw work.

  • Reuse geometry and materials when objects have the same shape and surface.
  • Instance repeated meshes when separate objects would produce many draw calls.
  • Keep constructor arguments stable; changing R3F args reconstructs the underlying object.
  • Load assets through the framework cache rather than starting a new load from each component.
  • Limit raycasts to interactive targets instead of the whole scene.
  • Size shadow maps, postprocessing buffers, and render targets to the quality the experience actually needs.

Optimize from measurements. A lower draw-call count is useful only if the frame trace or target hardware shows that draw submission is the constraint.

Verify renderer and viewport behavior

Exercise the scene at each supported viewport and device-pixel ratio. In plain Three.js, update the renderer size and camera projection when the canvas display size changes. Avoid resizing the drawing buffer on every frame when its size is already correct.

Check color textures and output color space separately from data textures. Normal, roughness, metalness, and similar data maps should not receive color space conversion. Confirm tone mapping, exposure, lights, and environment maps with a stable reference frame before changing material values to compensate.

Review the rendered result

Capture evidence after initial load, after moving the camera, after the primary interaction, and after resizing. Check:

  • the intended subject remains framed and controls do not clip through it
  • opaque, transparent, and transmissive surfaces sort as intended
  • seams, coplanar surfaces, and shadows remain stable while the camera moves
  • textures keep their orientation and detail at near and grazing views
  • loading, empty, error, and reduced-motion states remain usable
  • keyboard and pointer actions produce the same state changes the UI promises
  • essential information and state in the canvas also have a DOM or text equivalent that assistive technology can read
  • a WebGL initialization failure or context loss produces a usable fallback instead of an unexplained blank region

Do not report a visual failure from source alone. If the scene cannot be run, label the item as a source-based risk and name the missing runtime evidence.

Fix one measured cause at a time

Start with crashes, context loss, unbounded resource growth, and hot-path work that appears in the performance trace. Then address interaction and visual failures. Keep aesthetic refinements separate from performance changes so the evidence still identifies what moved the result.

After each fix:

  1. Repeat the exact baseline interaction.
  2. Compare frame timing and renderer.info under the same conditions.
  3. Revisit every viewport and visual state affected by the change.
  4. Mount, interact, unmount, and remount to exercise cleanup.
  5. Confirm the console has no new warnings or WebGL errors.

A completed audit links each change to a reproduced symptom, a measured or captured result, and the checks that guard it from returning.

Primary references

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-three-js-guide({ topic: "auditing-and-fixing-a-scene" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems