Rebuild on change

Rebuild strategy

  • Geometry rebuilds triggered by a parameter change must be debounced (100–200ms), so dragging a slider does not rebuild per frame and drop the frame rate.
  • A rebuild must be scoped to the content group's subtree. Camera position, OrbitControls state and the ambient-motion rig must not reset because a parameter changed.

Disposing resources

Two different lifetimes — do not collapse them into one dispose function.

On every rebuild, release what this build created:

  • every geometry.dispose()
  • every material.dispose(), plus any texture created for this build

Skip this and GPU memory climbs the whole time someone is adjusting parameters, until the page locks up.

On unmount only, release what outlives a rebuild:

  • the PMREM render target and the environment map
  • textures shared across rebuilds — the procedural noise texture, a CanvasTexture shared with the 2D canvas (see Render quality)
  • controls.dispose() and renderer.dispose()

Neither list tolerates items from the other. Regenerating the environment map every rebuild costs a multi-pass PMREM render per debounce tick — more expensive than the leak you were avoiding. Disposing a shared texture every rebuild is worse: the next frame renders against a dead texture and the material goes blank.

Note that walking the content subtree and disposing every texture you find there hits both traps at once — a material references shared textures without owning them. Dispose what you created, not what you referenced.

Sizing, DPR and zero-size mount

  • renderer.setPixelRatio() must be clamped — Math.min(window.devicePixelRatio, 2). Rendering at DPR 3 on a phone or a 5K display costs 2.25× the fragments of DPR 2 for no visible gain, and is the most common reason a preview that runs fine on the developer's machine drops frames on the user's.
  • Reacting to a container resize means all three of camera.aspect, camera.updateProjectionMatrix() and renderer.setSize(). Miss updateProjectionMatrix() and the image stretches; miss setSize() and it renders at the old resolution.
  • Your component can mount at 0×0 — inside a hidden tab or a collapsed panel. When ResizeObserver reports 0×0 you must ignore it and keep the current view state, then initialise or refit once a non-zero size arrives. That is what keeps the view intact across a tab switch.
Esc
Search all docs · to navigate · to open