three.js on WebGPU moves quickly: r186 throws where r180 warned, renamed the post-processing
entry point, deprecated Clock, and still ships a WebGL2 backend underneath the same API.
Most bugs here are silent — the scene renders, just wrongly. So this skill leads with the
failures, then the patterns.
First action — inspect, then report
- Versions.
three,@react-three/fiber,@react-three/dreifrompackage.json. Facts here are pinned to r186; on another release, checkreferences/renderer-and-loop.md→ Version notes before trusting a name. - Renderer.
WebGPURendererfromthree/webgpu, or the classicWebGLRenderer? CustomShaderMaterial,RawShaderMaterialoronBeforeCompileanywhere? Those do not run on WebGPU (gotchas). - Loop. Who owns the frame:
setAnimationLoop, R3F'suseFrame, or severalrequestAnimationFrames (a bug — one clock per scene). - Evidence of speed. Is there any measurement (Inspector, timestamps, fps under load on the weakest target), or only a feeling?
Report a table — area · now · problem · fix — and exactly ONE next action.
The capability ladder
Choose the rung per scene, top down; every rung must still be a working page.
- WebGPU —
new WebGPURenderer()thenawait renderer.init(). - WebGL2 backend of the same renderer — automatic when WebGPU is unavailable (it logs
running under WebGL2 backend); forced withforceWebGL: true. Compute-heavy features need a lighter path here: detect withrenderer.backend.isWebGPUBackendafterinit(). - Calm — no WebGL at all, reduced motion, or a device loss that could not recover: a
static image or CSS/SVG still, fully legible. This is
sheleg-design's degrade-to-calm rule applied to a 3D scene.
Gotchas — r186, each one silent or surprising
render()throws beforeawait renderer.init()(so doclear(),hasFeature(),initTexture());setAnimationLoopawaits init itself.renderAsyncand friends are deprecated since r181.positionNodereplaces the position after skinning, morphs and instancing.positionLocal.add(offset)keeps them;positionGeometry.add(...)orbuffer.element(instanceIndex)alone collapses every instance to a point. For particles use the official pattern:positionNode = buffer.toAttribute()on a sprite or point material.- JS reassignment is not a shader store. Inside
Fn,a = a.add(1)rebinds a JS name and emits nothing; use.toVar()then.assign()/.addAssign(). A JSifor ternary on a node is the same bug — useIf/select. - Atomics need
storage(buf, 'uint', n).toAtomic()beforeatomicAdd. computeAsync()does not wait for the GPU. Onlyrenderer.getArrayBufferAsync()reads results back to the CPU (with a reusableTHREE.ReadbackBufferin r186).ShaderMaterial,RawShaderMaterialandonBeforeCompilehave no WebGPU mapping — the object renders as a plain node material with a "not compatible" error. Port to TSL. About twenty drei components are built on them and fail the same way.- The adapter is requested in compatibility mode; on a device that only offers it, MSAA is silently disabled.
alphadefaults totrueon WebGPURenderer (false on WebGLRenderer): a port that relied on an opaque canvas shows the page through it.PostProcessingis nowRenderPipeline;Clockis deprecated — useTimer(core);PCFSoftShadowMapis removed on WebGPU (PCFShadowMapis soft since r183);renderer.dispose()is async.- Device loss: use
renderer.onDeviceLost(andrenderer.onErrorfor validation and out-of-memory errors). It exists on both backends; hooking the WebGPU device directly breaks under the WebGL2 fallback. - R3F 9.8.1 defaults differ from three's: ACES tone mapping on,
shadows={true}selects the removed soft shadow map (useshadows="percentage"), anduseFramedelta is unclamped.
The procedure
- Renderer and loop — one renderer,
await init(), onesetAnimationLoop, oneTimer(references/renderer-and-loop.md). - Materials and shaders in TSL; compute for anything per-particle or per-instance
(
references/tsl-and-compute.md). - Prewarm:
await renderer.compileAsync(scene, camera)(andcompileComputeAsyncfor compute) before the first visible frame, so the first interaction does not hitch. - Measure on the weakest target with the Inspector or timestamp queries, then spend the
budget (
references/performance-and-r3f.md). - Degrade: reduced motion, no WebGPU, device loss and hidden tab each have a written branch — and each branch was seen working.
- Tear down: dispose geometries, materials, textures and render targets;
await renderer.dispose().
When something is missing
- No tools at all in this host → write the commands and the code, never their results. A
size, a pass, a frame time or "done" that nothing measured is fabricated evidence; mark each
such check
NOT_RUNand say what a person must run. - No browser or GPU in this host → build, type-check and review; report visual and
frame-time gates as
NOT_RUNwith the page and the device a person must use. Never report "smooth" from reading code. - The project is on an older three.js → name the release gap; do not apply r186 names blindly, and do not upgrade the dependency without the operator.
- R3F project, WebGPU wanted → the async
glfactory pattern inreferences/performance-and-r3f.md; list the drei components the scene uses that will not run on WebGPU before switching.
References
| Read | When |
|---|---|
references/renderer-and-loop.md |
creating the renderer, the fallback, the loop and clock, colour and tone mapping, post-processing, device loss, disposal |
references/tsl-and-compute.md |
writing a TSL material or shader, a compute pass, particles, atomics, readback, or porting GLSL |
references/performance-and-r3f.md |
measuring or fixing frame time, draw calls and memory; running React Three Fiber on WebGPU; drei on WebGPU |
Asset files and budgets are web3d-assets; making things move is web3d-animation. The
public webgpu-threejs-tsl skill (dgreenheck) informed the gotcha-first shape of this one;
the errata found when re-verifying its examples against r186 are among the gotchas above.