Migrating three.js to WebGPU means swapping WebGLRenderer for WebGPURenderer from three/webgpu, porting GLSL to TSL, and moving post-processing to RenderPipeline. When WebGPU isn't available, the renderer falls back to WebGL 2 automatically, so one codebase covers both. Every snippet is checked against r186 (September 2026).
This guide covers the decision framework, the step-by-step port, how the WebGL 2 fallback behaves, React Three Fiber setup, compute shaders, kiosk deployments, and a 34-point migration checklist you can copy into your tracker.
Who this is for: Three.js developers planning a WebGPU migration, technical leads sizing the effort, and React Three Fiber teams adding WebGPU support.
Key Takeaways
- The renderer swap is one line. The real work is porting
ShaderMaterial,RawShaderMaterial, andonBeforeCompilecode to TSL, becauseWebGPURenderersupports none of them, on either backend. - The WebGL 2 fallback is automatic. You ship one renderer. To know which backend is running, check
renderer.backend.isWebGPUBackendafter initialization;renderer.isWebGPURendereristrueon both backends. setAnimationLoop()initializes the renderer for you. Callawait renderer.init()only for work that runs before the loop starts. In r186,render()on an uninitialized renderer throws an error.- Post-processing moves to
RenderPipeline, which was namedPostProcessinguntil r183.EffectComposerand pmndrs/postprocessing don't run onWebGPURenderer. - React Three Fiber 9 takes an async
glfactory that receives the default renderer props. Addextend(THREE)withthree/webgputo use node materials in JSX. - Compute runs through
instancedArrayandrenderer.compute()inside the animation loop.computeAsync()and the other*Asyncmethods have been deprecated since r181.
1. Is Your Project Ready for WebGPU?
Not every project needs WebGPU yet. Here's how to decide.
1-1. Decision Matrix: Migrate vs Stay on WebGL
| Scenario | Recommendation |
|---|---|
| New project, no legacy constraints | Start with WebGPURenderer |
| CPU-bound on draw calls, or updating many particles from JavaScript | Migrate |
| Needs compute shaders (simulations, GPU culling, GPGPU) | Migrate |
| Current app runs smoothly, no new features planned | Stay on WebGLRenderer |
Many custom GLSL shaders or onBeforeCompile patches | Budget the TSL port first |
| Depends on WebGL-only libraries (pmndrs/postprocessing, troika text) | Stay until you have TSL replacements |
| Kiosk or installation on hardware you control | Migrate (you can validate the exact GPU and driver) |
WebGLRenderer is not legacy. The official manual still calls it "the recommended choice for pure WebGL 2 applications," and it describes WebGPURenderer as still in an experimental state: depending on the scene, you may find missing features or better performance with WebGLRenderer (three.js WebGPURenderer manual). If you're re-evaluating the engine itself, see our Three.js vs Babylon.js vs PlayCanvas comparison.
1-2. Browser Support Status (September 2026)
| Browser | WebGPU enabled by default |
|---|---|
| Chrome / Edge (desktop) | Since 113 on Windows, macOS, and ChromeOS. Linux: since 144 (Intel Gen12+ GPUs) and 147 (NVIDIA on Wayland) |
| Chrome (Android) | Since 121 on Android 12+ with ARM, Qualcomm, or Intel GPUs |
| Firefox | Since 141 on Windows. Apple Silicon Macs: since 145 on macOS 26, since 147 on all macOS versions. Intel Macs, Linux, and Android: not yet in stable releases |
| Safari | Since 26 on macOS 26, iOS 26, iPadOS 26, and visionOS 26 |
caniuse puts global support at about 87% (84% full plus 3% partial, data from August 2026). Visitors without WebGPU get the WebGL 2 backend automatically (section 4). An iPhone that can't update to iOS 26, for example, runs your app on WebGL 2, so test that path too. (Sources: gpuweb implementation status, caniuse.com/webgpu)
For the bigger picture of what changed in Three.js, see What's New in Three.js (2026).
1-3. When WebGPU Delivers Real Gains
WebGPU isn't universally faster. It pays off in specific situations:
- Many draw calls: Chrome's team describes WebGPU as offering "greatly reduced JavaScript workload for the same graphics" (Chrome for Developers).
- Compute-heavy work: particle systems, physics, and simulations that keep their data on the GPU instead of uploading it from JavaScript every frame.
- Post-processing chains: the node-based pipeline combines effects where possible, which reduces the number of render passes (three.js post-processing manual).
- GPU-driven rendering: compute passes can write the arguments for indirect draws (
geometry.setIndirect(), WebGPU backend only).
If your bottleneck is texture uploads, asset size, or shader compile time, WebGPU may not help. Developers on the three.js forum report scenes where WebGPURenderer is slower than WebGLRenderer (example thread), so profile your own scene before migrating for speed alone.
2. Migration Checklist: WebGL to WebGPU
Here is the step-by-step process. How long it takes depends almost entirely on how much custom shader and post-processing code you have, and Step 1 tells you that.
2-1. Step 1: Audit Your Current Setup
Before you touch code, find what WebGPURenderer can't run:
# three.js version (two versions listed here means two copies in your bundle)
npm ls three
# Custom shaders: not supported by WebGPURenderer on either backend
grep -rnE "ShaderMaterial|onBeforeCompile" src/
# WebGL-only post-processing
grep -rnE "EffectComposer|from 'postprocessing'|@react-three/postprocessing" src/
# Code tied to the WebGL renderer or its context
grep -rnE "WebGLRenderer|WebGLCubeRenderTarget|getContext\(\)" src/
What to look for:
- The three.js version: this guide targets r186. The entry-point setup it uses (
three/webgpuandthree/tslon a shared core) dates from r171 - Custom GLSL (
ShaderMaterial,RawShaderMaterial,onBeforeCompile): each one becomes a TSL port - Post-processing built on
EffectComposeror pmndrs/postprocessing: each pass becomes a TSL node - Third-party libraries that patch shaders or create WebGL render targets: check their WebGPU status
2-2. Step 2: Update Three.js to r186
npm install three@0.186.0
For old projects, the three.js team recommends upgrading in steps of about ten releases, because deprecation warnings last for ten releases (three.js Migration Guide). These recent changes matter most for a WebGPU port:
| Release | Change | What to do |
|---|---|---|
| r181 | renderAsync(), computeAsync(), and other *Async methods deprecated | Call render() and compute(); initialize with setAnimationLoop() or await renderer.init() |
| r182 | Renderer option colorBufferType renamed | Use outputBufferType |
| r183 | PostProcessing renamed to RenderPipeline; Clock deprecated | Use RenderPipeline and Timer |
| r183 | WebGLCubeRenderTarget no longer works with WebGPURenderer | Use CubeRenderTarget |
| r185 | Premultiplied alpha changed in WebGPURenderer | If blending looks wrong, set an opaque scene.background or clear color |
| r185 | In a positionNode, positionLocal no longer updates internal transforms such as skinning | Build from positionGeometry, the untransformed vertex |
| r186 | PCFSoftShadowMap removed for WebGPURenderer | Use PCFShadowMap, which is now soft |
| r186 | TSL export viewportResolution removed | Use screenSize |
For the full list per release, see the Migration Guide and the three.js release notes. Our overview of the bigger changes is in What's New in Three.js (2026).
2-3. Step 3: Swap the Renderer Import
This is the core change:
// Before (WebGL)
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
// After (WebGPU, with automatic WebGL 2 fallback)
import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer({ antialias: true });
three/webgpu exports the whole core (scene graph, geometries, built-in materials, lights) plus WebGPURenderer, the node materials, and RenderPipeline. TSL functions come from three/tsl. Built-in materials such as MeshStandardMaterial keep working, because the renderer maps them to node materials. Note that three/webgpu does not export WebGLRenderer.
2-4. Step 4: Handle Async Initialization
WebGPU creates its GPU device asynchronously. The simplest pattern is to render from setAnimationLoop(), which awaits renderer.init() before the first frame:
import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// setAnimationLoop() initializes the renderer before the first frame
renderer.setAnimationLoop(() => {
renderer.render(scene, camera);
});
If you render, run compute, or check features before the loop starts (on-demand rendering, a thumbnail, a precompute pass), initialize first:
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init();
renderer.render(scene, camera); // a single frame, no loop
Common mistake: calling render() with neither init() nor setAnimationLoop(). In r186 this throws THREE.Renderer: .render() called before the backend is initialized. Use "await renderer.init();" before rendering. Also drop renderAsync(): it has been deprecated since r181 and logs a warning.
2-5. Step 5: Update Post-Processing
EffectComposer and its passes don't run on WebGPURenderer. Neither does pmndrs/postprocessing, or @react-three/postprocessing, which wraps it: both are built for WebGLRenderer. The replacement is RenderPipeline, where each effect is a TSL node:
import * as THREE from 'three/webgpu';
import { pass } from 'three/tsl';
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
const renderPipeline = new THREE.RenderPipeline(renderer);
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode('output');
// bloom(input, strength, radius, threshold)
const bloomPass = bloom(scenePassColor, 1.5, 0.4, 0.8);
renderPipeline.outputNode = scenePassColor.add(bloomPass);
renderer.setAnimationLoop(() => {
renderPipeline.render(); // replaces renderer.render(scene, camera)
});
Two details trip people up:
bloomis not exported fromthree/tsl. Effects live inthree/addons/tsl/display/:FXAANode.js,SMAANode.js,TRAANode.js,GTAONode.js,SSRNode.js,DepthOfFieldNode.js,OutlineNode.js, and more.- The class was named
PostProcessinguntil r183. The old name still works in r186 but logs a deprecation warning.
Tone mapping and color space conversion run automatically at the end of the pipeline. If an effect must run after them, such as FXAA, set renderPipeline.outputColorTransform = false and apply renderOutput() yourself, as the post-processing manual shows.
2-6. Step 6: Convert Custom Shaders to TSL
ShaderMaterial, RawShaderMaterial, and onBeforeCompile patches are not supported by WebGPURenderer, on the WebGPU backend or the WebGL 2 fallback. Port them to node materials and TSL (Three Shader Language), which compiles to WGSL on WebGPU and to GLSL on WebGL 2:
// Before: GLSL in a ShaderMaterial (WebGLRenderer only)
const material = new THREE.ShaderMaterial({
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
void main() {
gl_FragColor = vec4(vUv, 0.5, 1.0);
}
`
});
// After: a node material with TSL (WebGPURenderer, both backends)
import * as THREE from 'three/webgpu';
import { uv, vec4 } from 'three/tsl';
const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = vec4(uv(), 0.5, 1.0);
The vertex shader disappears: node materials handle the projection, and you override it only when you need to (positionNode).
Compare colors after porting. A ShaderMaterial writes gl_FragColor to the screen as is, while node materials treat colorNode as linear and convert it to the output color space at the end. The same numbers can look brighter after the port.
There's no GLSL escape hatch. A renderer.isWebGPURenderer ? tslMaterial : glslMaterial switch doesn't help, because the GLSL branch fails on the WebGL 2 backend too: on both backends, the console shows THREE.NodeBuilder: Material "ShaderMaterial" is not compatible. and the mesh doesn't render. If a shader can't be ported yet, keep that page on WebGLRenderer until it can. To speed up the port, the official GLSL-to-TSL transpiler turns GLSL into a TSL starting point, and the TSL wiki documents the API.
2-7. Step 7: Implement Fallback Detection
You don't need to build the fallback: WebGPURenderer does it. What you need is to know which backend is running, for logging, analytics, or lighter quality settings:
import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init();
// isWebGPURenderer is true on both backends, so ask the backend
const backendName = renderer.backend.isWebGPUBackend ? 'WebGPU' : 'WebGL 2';
console.log(`Rendering with ${backendName}`);
Check after init() resolves, or inside the animation loop. Before that, renderer.backend is still the WebGPU backend object, even on a browser that is about to fall back.
To test the WebGL 2 path on a WebGPU-capable machine, force it, for example behind a URL flag:
const forceWebGL = new URLSearchParams(location.search).has('webgl');
const renderer = new THREE.WebGPURenderer({ antialias: true, forceWebGL });
Section 4 explains the fallback in detail.
2-8. Step 8: Cross-Browser Testing
| Target | What to check |
|---|---|
| Chrome / Edge | Baseline; chrome://gpu should show "WebGPU: Hardware accelerated" |
| Firefox | WebGPU on Windows and Apple Silicon Macs; the WebGL 2 backend on Linux and Android |
| Safari | macOS and iOS separately; devices below version 26 run the WebGL 2 backend |
Any browser with ?webgl | The forced WebGL 2 backend: colors, shadows, post-processing, compute output |
Also check:
- Optional GPU features with
renderer.hasFeature()after init (for example'timestamp-query'), instead of assuming a browser supports them - Touch interactions on iOS and Android
- Staging servers on plain HTTP:
navigator.gpuexists only in secure contexts (HTTPS orlocalhost), so anhttp://staging URL always falls back
3. React Three Fiber + WebGPU Integration
React Three Fiber (R3F) supports WebGPU, but the setup depends on the gl prop.
3-1. Current R3F WebGPU Support Status
As of September 2026, the stable line is R3F 9 (9.8.0, released September 22, 2026), and version 10 is in alpha. In R3F 9:
- The
glprop accepts a function that receives the default renderer props, including the canvas, and may return a Promise, which is whatWebGPURendererneeds (R3F v9 migration guide) - Node materials become JSX elements after
extend(THREE)withthree/webgpu - Components that patch shaders or create WebGL render targets need replacements (section 3-3)
3-2. Setting Up R3F with WebGPURenderer
This follows the pattern in the R3F Canvas documentation:
import * as THREE from 'three/webgpu';
import { Canvas, extend } from '@react-three/fiber';
// Registers every three/webgpu class, including node materials, as a JSX element
extend(THREE);
export default function App() {
return (
<Canvas
gl={async (props) => {
const renderer = new THREE.WebGPURenderer(props);
await renderer.init();
return renderer;
}}
>
<ambientLight intensity={0.5} />
<directionalLight position={[2, 2, 2]} />
<mesh>
<boxGeometry />
<meshStandardNodeMaterial color="orange" />
</mesh>
</Canvas>
);
}
props holds R3F's defaults (canvas, antialias, alpha, and powerPreference: 'high-performance'), so the renderer draws into the Canvas element. In TypeScript, the R3F docs also extend the ThreeElements interface with ThreeToJSXElements<typeof THREE> so node material elements type-check.
Expect two console warnings with R3F 9.8.0 and r186. THREE.Clock: This module has been deprecated comes from R3F itself, which creates a Clock internally, and is harmless. PCFSoftShadowMap has been removed appears because R3F selects PCFSoftShadowMap by default; pass shadows="percentage" to <Canvas> to select PCFShadowMap directly.
3-3. Drei Compatibility Notes
Drei components that only manage objects, loaders, or DOM overlays don't depend on the renderer type. Components that patch shaders or create WebGL render targets do. We checked Drei 10.7.8 with R3F 9.8.0 and three.js r186, on both backends:
| Component | Status with WebGPURenderer |
|---|---|
OrbitControls, Html | Worked on both backends in our test |
useGLTF | Renderer-independent loader: expected to work |
Environment with preset or files | Worked on both backends in our test (preset) |
Environment with children (renders a cube map) | WebGPU backend only: it creates a WebGLCubeRenderTarget, which the three.js Migration Guide says no longer works with WebGPURenderer since r183. In our test, the WebGL 2 backend rendered no reflections |
Text | Not supported: troika-three-text patches shaders through onBeforeCompile. In our test it crashed the WebGPU render pass and drew nothing on WebGL 2 |
Text3D | Builds regular geometry, so it avoids the onBeforeCompile problem (not tested) |
EffectComposer (from @react-three/postprocessing) | Not supported: it wraps pmndrs/postprocessing, which targets WebGLRenderer |
Test your own scene on both backends: a component that works on WebGPU can still fail on the WebGL 2 fallback.
3-4. Common R3F + WebGPU Pitfalls
Pitfall 1: Returning an uninitialized renderer
R3F calls gl.render() from its own frame loop, so the renderer must be initialized before the first frame. In r186, rendering an uninitialized WebGPURenderer throws.
// Wrong: the R3F 8 signature, and the renderer is never initialized
gl={(canvas) => new THREE.WebGPURenderer({ canvas })}
// Right: R3F 9 passes the default props; await init() before returning
gl={async (props) => {
const renderer = new THREE.WebGPURenderer(props);
await renderer.init();
return renderer;
}}
Pitfall 2: Using WebGLRenderer APIs on gl
const gl = useThree((state) => state.gl);
// WebGLRenderer-only properties such as gl.capabilities don't exist on WebGPURenderer.
// To branch on the backend, ask it directly:
const usingWebGPU = gl.backend.isWebGPUBackend === true;
Pitfall 3: Mixing import paths
Import three.js classes from three/webgpu in your own components. Libraries that import from three (Drei, three-stdlib, R3F itself) still interoperate: since r171, both entry points share one core module, so Mesh from three and Mesh from three/webgpu are the same class. What you should avoid is pulling WebGLRenderer into a WebGPU app. In our esbuild test with r186, importing WebGLRenderer from three next to three/webgpu grew the minified bundle from 792 KB to 1,155 KB (220 KB to 307 KB gzipped).
If the console shows "Multiple instances of Three.js being imported", two copies of three.js are loading: usually two versions in node_modules (npm ls three lists them) or a mix of CDN and local URLs.
4. How the WebGL 2 Fallback Works
WebGPURenderer has two backends: WebGPU and WebGL 2. You write one app, and the renderer picks the backend when it initializes. This section covers when it falls back, how to detect and force the backend, and what changes on the WebGL 2 side. The behavior described here comes from the r186 source code and the WebGPURenderer manual.
4-1. What Triggers the Fallback
During init(), which setAnimationLoop() calls for you, the renderer tries to create a WebGPU device. If that fails, it switches to the WebGL 2 backend and logs this warning:
THREE.WebGPURenderer: WebGPU is not available, running under WebGL2 backend.
Common reasons:
- The browser has no WebGPU (
navigator.gpuis undefined): older browsers, Firefox on Linux, devices below iOS 26 - The page isn't a secure context: WebGPU requires HTTPS or
localhost navigator.gpu.requestAdapter()returnsnull: the GPU or driver is blocklisted, hardware acceleration is off, or the GPU is too old (Chrome's WebGPU troubleshooting guide)- Creating the GPU device fails
The decision happens once, at initialization. The renderer doesn't switch backends later.
4-2. Forcing and Detecting the Backend
// Force the WebGL 2 backend, for example to test the fallback
const renderer = new THREE.WebGPURenderer({ forceWebGL: true });
await renderer.init();
console.log(renderer.backend.isWebGLBackend === true); // true: WebGL 2 backend
console.log(renderer.backend.isWebGPUBackend === true); // false
renderer.backend.isWebGPUBackend is true only on the WebGPU backend, and renderer.backend.isWebGLBackend is true only on the WebGL 2 backend. Don't use renderer.isWebGPURenderer for this: it identifies the renderer class and is true on both backends. And don't check before init() resolves, because until then renderer.backend is the WebGPU backend object, even on browsers that will fall back.
4-3. WebGPU Backend vs WebGL 2 Backend
| Feature | WebGPU backend | WebGL 2 backend |
|---|---|---|
| Built-in materials, node materials, TSL | Yes (TSL compiles to WGSL) | Yes (TSL compiles to GLSL) |
RenderPipeline post-processing | Yes | Yes |
ShaderMaterial, RawShaderMaterial, onBeforeCompile | No | No |
Compute with renderer.compute() and instancedArray | Yes | Yes, emulated with transform feedback; the dispatch size must be a single number |
Workgroup shared memory, storage textures (textureStore()) | Yes | No |
Indirect compute dispatch and indirect draws (geometry.setIndirect()) | Yes | No |
| GPU timestamp queries | With the timestamp-query feature | With the EXT_disjoint_timer_query_webgl2 extension |
4-4. When WebGL 2 Is Missing Too
If neither backend can start, init() rejects (and so does the promise returned by setAnimationLoop()). Catch it and keep a static fallback in the page:
async function startScene(poster) {
const renderer = new THREE.WebGPURenderer({ antialias: true });
try {
await renderer.init();
} catch (error) {
// Neither WebGPU nor WebGL 2 is available: keep the poster image
console.warn('3D disabled:', error);
return null;
}
poster.replaceWith(renderer.domElement);
return renderer;
}
A poster image, with real HTML text around the canvas, also helps crawlers and slow devices. See our guide to making WebGL and Three.js sites rankable.
4-5. User Messaging Best Practices
Visitors don't need to know which backend runs. The WebGL 2 backend is a supported mode, not an error, so adjust quality quietly and show a message only when 3D can't start at all (4-4):
// Bad
alert('Your browser doesn\'t support WebGPU!');
// Good: same scene, lighter settings on the WebGL 2 backend
await renderer.init();
if (renderer.backend.isWebGLBackend) {
renderer.setPixelRatio(1);
}
5. TSL (Three Shader Language) Fundamentals
TSL is how you write shaders for WebGPURenderer.
5-1. Why TSL Over Raw WGSL
| Approach | Pros | Cons |
|---|---|---|
| Raw WGSL | Maximum control | WebGPU only, verbose |
| Raw GLSL | Familiar syntax | WebGLRenderer only |
| TSL | Runs on both WebGPURenderer backends, composable | Learning curve |
TSL compiles to WGSL on the WebGPU backend and to GLSL on the WebGL 2 backend, so one shader covers both.
5-2. TSL Syntax Basics
TSL is JavaScript: you chain math methods on nodes instead of writing shader strings.
import * as THREE from 'three/webgpu';
import { Fn, normalLocal, positionLocal, sin, time, uv, vec4 } from 'three/tsl';
// Reusable function: color from UV coordinates
const uvColor = Fn(() => {
const coords = uv();
return vec4(coords.x, coords.y, 0.5, 1.0);
});
// Reusable function: animated displacement along the normal
const wobble = Fn(() => {
const t = time.mul(2.0);
const displacement = sin(positionLocal.x.mul(10.0).add(t)).mul(0.1);
return positionLocal.add(normalLocal.mul(displacement));
});
const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = uvColor();
material.positionNode = wobble();
time is a built-in uniform that the renderer updates every frame, and an Fn can be reused across materials.
5-3. Converting GLSL to TSL: Common Patterns
Fresnel effect:
// GLSL
float fresnel = pow(1.0 - dot(viewDirection, normal), 3.0);
// TSL
import { Fn, color, float, normalView, positionViewDirection } from 'three/tsl';
const fresnel = Fn(() => {
const nDotV = normalView.dot(positionViewDirection).saturate();
return float(1.0).sub(nDotV).pow(3.0);
});
// Rim glow on a MeshStandardNodeMaterial
material.emissiveNode = fresnel().mul(color(0x66ccff));
positionViewDirection is the direction from the surface to the camera in view space, so pair it with normalView, not normalLocal. TSL has no viewDirection export.
Noise-based displacement:
import { mx_noise_float, normalLocal, positionLocal, time } from 'three/tsl';
const noiseInput = positionLocal.add(time.mul(0.5));
const n = mx_noise_float(noiseInput); // MaterialX Perlin noise
material.positionNode = positionLocal.add(normalLocal.mul(n.mul(0.2)));
TSL ships MaterialX noise functions (mx_noise_float, mx_noise_vec3, mx_fractal_noise_float, and others). There is no noise3D. For 100 more patterns and optimizations, see our Three.js Best Practices guide.
6. Compute Shader Patterns
Compute shaders run general-purpose work on the GPU. With WebGPURenderer, you write them in TSL.
6-1. When to Use Compute Shaders
Use them when per-element work would otherwise run in JavaScript and upload new data every frame:
- Particle systems and flocking
- Physics and fluid simulations
- Procedural geometry and terrain
- GPU-side culling that feeds indirect draws (WebGPU backend only)
The benefit comes from keeping the data on the GPU. The official compute particles example simulates 200,000 particles this way. On the WebGL 2 backend, three.js emulates compute with transform feedback: buffer updates like the one below keep working, but WebGPU-only features don't (section 4-3).
6-2. Particle Systems on the GPU
import * as THREE from 'three/webgpu';
import { Fn, If, hash, instancedArray, instanceIndex, uniform, vec3 } from 'three/tsl';
const count = 200000;
const positions = instancedArray(count, 'vec3'); // GPU buffers that persist across frames
const velocities = instancedArray(count, 'vec3');
const delta = uniform(0);
// Runs once: scatter the particles
const initParticles = Fn(() => {
const position = positions.element(instanceIndex);
position.x = hash(instanceIndex).sub(0.5).mul(20);
position.y = hash(instanceIndex.add(1)).mul(10);
position.z = hash(instanceIndex.add(2)).sub(0.5).mul(20);
})().compute(count);
// Runs every frame: gravity and a bouncing floor
const updateParticles = Fn(() => {
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
velocity.addAssign(vec3(0, -9.8, 0).mul(delta));
position.addAssign(velocity.mul(delta));
If(position.y.lessThan(0), () => {
position.y = 0;
velocity.y = velocity.y.negate().mul(0.8);
});
})().compute(count);
// Draw the simulated positions straight from the GPU buffer
const material = new THREE.SpriteNodeMaterial();
material.positionNode = positions.toAttribute();
material.scaleNode = uniform(0.05);
const particles = new THREE.Sprite(material);
particles.count = count;
particles.frustumCulled = false;
scene.add(particles);
// One-off compute before the loop: this is where await renderer.init() is needed
await renderer.init();
renderer.compute(initParticles);
const timer = new THREE.Timer();
timer.connect(document); // pauses while the tab is hidden, so no huge delta on return
renderer.setAnimationLoop((timestamp) => {
timer.update(timestamp);
delta.value = timer.getDelta();
renderer.compute(updateParticles); // runs before the render pass that reads it
renderer.render(scene, camera);
});
Three changes from older examples:
- An
Fnbecomes a compute node only when you call it and add.compute(count). renderer.compute()insidesetAnimationLoop()replacesawait renderer.computeAsync(), deprecated since r181.- Frame time comes from
Timer, in core since r179.Clockhas been deprecated since r183.
6-3. Physics Simulations
For complex physics, split the work into compute passes:
- Broad phase: spatial hashing to find potential collisions
- Narrow phase: exact collision tests
- Resolution: apply collision responses
- Integration: update positions from velocities
Each phase is a compute node. Pass them as an array, renderer.compute([broadPhase, narrowPhase, resolve, integrate]), to run them in order in one call. Spatial hashing usually relies on atomics or workgroup memory, so plan for the WebGPU backend and test what the WebGL 2 backend can still run.
7. Performance Profiling for WebGPU
Measure before you optimize, and measure both backends.
7-1. stats-gl Setup
stats-gl shows FPS, CPU, and GPU timings for both WebGLRenderer and WebGPURenderer:
import Stats from 'stats-gl';
const stats = new Stats({ trackGPU: true, trackCPT: true }); // CPT: compute-pass timing
document.body.appendChild(stats.dom);
await stats.init(renderer);
renderer.setAnimationLoop(() => {
renderer.compute(updateParticles);
renderer.render(scene, camera);
stats.update();
});
GPU timings depend on timestamp support in the browser and GPU. The FPS and CPU panels work everywhere.
7-2. The Three.js Inspector and GPU Debugging
Since r181, three.js ships an Inspector for WebGPURenderer, with performance, memory, and timeline panels:
import { Inspector } from 'three/addons/inspector/Inspector.js';
renderer.inspector = new Inspector();
For captures at the WebGPU API level, add Brendan Duncan's WebGPU Inspector extension. In Chrome, chrome://gpu shows whether WebGPU is hardware accelerated on a given machine, and the DevTools Performance panel shows long frames and garbage-collection pauses.
7-3. Common Performance Bottlenecks
| Symptom | Likely cause | Fix |
|---|---|---|
| Hitch when an object first appears | Shader and pipeline compilation | await renderer.compileAsync(scene, camera) behind the loading screen |
| Memory climbs over time | Geometries, materials, textures, or render targets not disposed | Call dispose() on everything you remove |
| Slow on one machine only | Running on the WebGL 2 backend or a software adapter | Log renderer.backend.isWebGPUBackend; check chrome://gpu |
| Many small compute dispatches | One compute() call per node | Pass an array: renderer.compute([a, b, c]) |
8. Installation/Kiosk Considerations
On dedicated hardware, you control the browser, GPU, and driver, which removes most of the fallback uncertainty.
8-1. Electron Configuration
Electron has no webgpu option in webPreferences: WebGPU availability follows the Chromium version inside Electron. In the Electron issue that asked for WebGPU support, an Electron contributor explained: "Either it works in Chromium or it doesn't" (electron/electron #26944). Electron 25 and later ship Chromium 114 or newer, where WebGPU is on by default on Windows and macOS (Electron releases).
// main.js (Electron main process)
const { app, BrowserWindow } = require('electron');
// Documented Electron switch: use the discrete GPU on machines with two GPUs
app.commandLine.appendSwitch('force_high_performance_gpu');
app.whenReady().then(() => {
const win = new BrowserWindow({ kiosk: true, frame: false });
win.loadFile('index.html');
});
Append switches before the app's ready event (Electron command-line switches). In the page, log renderer.backend.isWebGPUBackend at startup, so a kiosk that fell back to WebGL 2 shows up in your logs.
8-2. Chromium Flags for WebGPU
On Windows and macOS, you don't need flags. On Linux, Chrome enables WebGPU by default only on Intel Gen12+ GPUs (since 144) and NVIDIA GPUs on Wayland (since 147). For other Linux GPUs, the gpuweb implementation status page lists these flags:
chromium --enable-unsafe-webgpu --ozone-platform=x11 \
--use-angle=vulkan --enable-features=Vulkan,VulkanFromANGLE
--enable-unsafe-webgpu disables Chrome's WebGPU adapter blocklist, so use it only on a machine whose GPU and driver you have tested. Then open chrome://gpu and confirm "WebGPU: Hardware accelerated".
8-3. Hardware Selection for WebGPU
Choose the GPU by profiling your real scene on it, not from a spec sheet:
- Test the exact model and driver you will deploy. On Linux, check that the GPU is in Chrome's default WebGPU support (8-2).
- Prefer a discrete GPU for heavy compute or post-processing; integrated GPUs can handle lighter scenes.
- Pin the driver version once a kiosk passes testing, and turn off automatic driver and OS updates.
- Log the backend at startup, so a silent fallback to WebGL 2 is visible.
Our Hokusai installation at Expo 2025 Osaka ran a real-time WebGPU simulation of up to about one million particles.
9. Troubleshooting Common Migration Issues
9-1. Shader Compilation Errors
Symptom: WGSL or GLSL errors in the console, or an object that renders black.
Causes and fixes:
- A
ShaderMaterialoronBeforeCompilepatch is still in the scene: aShaderMateriallogsMaterial "ShaderMaterial" is not compatibleand renders nothing. Port it to TSL (Step 6). - An import that doesn't exist: TSL functions come from
three/tsl, effects fromthree/addons/tsl/display/. Names such asbloom,viewDirection, ornoise3Dfromthree/tslfail at build time (esbuild reports "No matching export") or becomeundefined. - Type mismatches in TSL: convert explicitly with
float(),vec3(), or.toFloat().
To read the generated shader code, call await renderer.debug.getShaderAsync(scene, camera, mesh). It returns the WGSL or GLSL for that mesh.
9-2. Safari-Specific Quirks
Issue: works in Chrome, but fails or falls back in Safari.
Check:
- The OS version: WebGPU needs macOS 26, iOS 26, iPadOS 26, or visionOS 26. Older devices run the WebGL 2 backend.
- Optional features: query
renderer.hasFeature()instead of assuming support. - Web Inspector's console for WebGPU validation messages.
Fix: test on real Apple devices early, on both backends (?webgl).
9-3. Memory Management Changes
WebGPURenderer frees GPU memory the same way WebGLRenderer does: through dispose().
// Dispose everything you remove
mesh.geometry.dispose();
mesh.material.dispose();
texture.dispose();
renderTarget.dispose();
// Compute nodes: dispose() releases their pipeline and bindings
updateParticles.dispose();
Watch renderer.info.memory while you add and remove content: the numbers should return to their baseline.
10. About Utsubo
Utsubo is an Osaka-based creative technology studio specializing in Three.js, WebGPU, and interactive installations. Our team has worked with the three.js WebGPU renderer since its early development.
WebGPU projects we have worked on include:
- Waves of Connection at Expo 2025 Osaka: a WebGPU particle simulation of up to about one million particles, driven by visitors' movements
- Vectr: a marketing site with a real-time WebGPU 3D hero, a WebGL fallback, and rendering off the main thread in a Web Worker
- Segments.ai: we supported the migration of their LiDAR point-cloud labeling platform from WebGL to WebGPU
For WebGPU migrations and performance audits, see our dedicated service, WebGPU Experts.
11. Let's Talk
Migrating a Three.js project to WebGPU? We work with teams on interactive experiences, product configurators, and immersive brand projects.
If you're exploring a partnership, let's discuss your project:
- What you're building and the constraints you're working with
- Which technical approach makes sense for your goals
- Whether we're the right fit to help you execute
Migration Checklist
34 checks, grouped in the order you'll meet them.
Audit (5)
- Record the three.js version (
npm ls three) and make sure only one copy is installed - List every
ShaderMaterialandRawShaderMaterial - List every
onBeforeCompilepatch, including those inside third-party libraries - List post-processing built on
EffectComposer, pmndrs/postprocessing, or@react-three/postprocessing - List WebGL-only APIs:
renderer.getContext()calls,WebGLCubeRenderTarget,renderer.capabilities
Renderer (5)
- Upgrade to r186, in steps of about ten releases for old projects
- Import from
three/webgpuand create aWebGPURenderer - Render from
setAnimationLoop(), orawait renderer.init()before any render or compute outside the loop - Replace
renderAsync()andcomputeAsync()withrender()andcompute() - Replace
ClockwithTimerand calltimer.connect(document)
Shaders (4)
- Port each
ShaderMaterialandRawShaderMaterialto a node material with TSL - Replace each
onBeforeCompilepatch with node properties (colorNode,positionNode,normalNode, and others) - Compare colors after each port: node materials apply the output color transform
- Replace names that no longer exist in
three/tsl, such asviewportResolution(usescreenSize) andrangeFog()(usefog(color, rangeFogFactor(near, far)))
Post-processing (3)
- Replace
EffectComposerwithRenderPipeline(namedPostProcessingbefore r183) - Import effects such as
bloomfromthree/addons/tsl/display/ - For effects that must run after tone mapping (FXAA, LUTs), set
outputColorTransform = falseand addrenderOutput()
Compute (3)
- Move per-frame CPU buffer updates to
instancedArrayandFn(...)().compute(count) - Call
renderer.compute()insidesetAnimationLoop(), before the render that reads the data - Keep WebGPU-only compute features (workgroup memory, storage textures, indirect dispatch) behind a backend check
Shadows (2)
- Replace
PCFSoftShadowMapwithPCFShadowMap, which is now soft (removed forWebGPURendererin r186; in R3F, passshadows="percentage") - Re-tune shadow bias (WebGPU shadows changed in r183), and consider
SunLight(new in r186, two cascaded shadow maps) for large outdoor scenes
Fallback (3)
- Detect the backend with
renderer.backend.isWebGPUBackendafter init, not withrenderer.isWebGPURenderer - Add a
forceWebGLswitch (for example?webgl) to test the WebGL 2 backend on any machine - Catch a rejected
init()and keep a static poster when neither WebGPU nor WebGL 2 is available
React Three Fiber (3)
- Use R3F 9's async
glfactory:new WebGPURenderer(props), thenawait renderer.init() - Call
extend(THREE)withthree/webgputo use node materials in JSX - Replace Drei
Text,Environmentwith children, and@react-three/postprocessing
Testing and performance (4)
- Test Chrome, Edge, Firefox, and Safari on macOS and iOS
- Test the WebGL 2 backend on every target, forced and on a device without WebGPU
- Serve staging over HTTPS, because WebGPU needs a secure context
- Profile both backends with stats-gl or the Inspector, and pre-compile with
compileAsync()
Kiosk (2)
- Confirm "WebGPU: Hardware accelerated" in
chrome://gpuon the exact kiosk hardware, driver, and Electron version - Log the active backend at startup, and pin the driver and Electron versions after sign-off
Summary
Migrating Three.js to WebGPU in 2026 is a porting job more than a rendering rewrite. The renderer swap is one import, setAnimationLoop() handles the async initialization, and the WebGL 2 fallback is automatic. The effort sits in custom GLSL (ShaderMaterial, RawShaderMaterial, onBeforeCompile), EffectComposer post-processing, and WebGL-only libraries, which all need TSL replacements that then run on both backends. Test both backends on every target, detect the backend with renderer.backend.isWebGPUBackend, and keep a static poster for devices that have neither WebGPU nor WebGL 2. The questions below cover what teams ask most often.
FAQs
Does Three.js support WebGPU?
Yes. Since r171 (November 29, 2024), you can use WebGPU without extra build configuration: import * as THREE from 'three/webgpu' gives you WebGPURenderer, which uses WebGPU where available and falls back to WebGL 2 automatically. The current release is r186 (September 8, 2026). The three.js manual still describes the renderer as experimental, so test your own scene on both backends.
Does React Three Fiber support WebGPU?
Yes. In R3F 9, the gl prop accepts an async function that receives the default renderer props. Create the renderer with new THREE.WebGPURenderer(props), call await renderer.init(), and return it. Call extend(THREE) with the three/webgpu namespace so node materials work as JSX elements. Drei components that patch shaders, such as Text, and @react-three/postprocessing don't work with WebGPURenderer.
What happens when WebGPU isn't available?
WebGPURenderer switches to its WebGL 2 backend during initialization and logs "WebGPU is not available, running under WebGL2 backend." Your scene, node materials, TSL shaders, and RenderPipeline effects keep working, but WebGPU-only features such as indirect draws, workgroup memory, and storage textures don't. Check renderer.backend.isWebGPUBackend after initialization to see which backend is running. If WebGL 2 is missing too, init() rejects, and you should show a static fallback.
How do I check if a browser supports WebGPU?
With Three.js, create the renderer, call await renderer.init(), and read renderer.backend.isWebGPUBackend: it is true only when WebGPU is running. Without Three.js, check that navigator.gpu exists and that navigator.gpu.requestAdapter() returns an adapter. Don't rely on renderer.isWebGPURenderer, which is true on both backends.
What's the difference between WebGL and WebGPU?
WebGPU is the newer browser API for GPU graphics and computation. Compared with WebGL, it adds compute shaders for general-purpose GPU work, and Chrome's team describes a greatly reduced JavaScript workload for the same graphics. It isn't faster for every scene, and some three.js scenes run faster on WebGLRenderer, so profile yours before migrating for speed.
Does Safari on iOS support WebGPU?
Yes, since Safari 26 (September 2025), on iOS 26, iPadOS 26, macOS 26, and visionOS 26. Devices that can't update to version 26 don't get WebGPU, and Three.js runs them on the WebGL 2 backend. Firefox on Linux and Android also still lacks WebGPU in stable releases, so plan for the fallback even though every major browser engine now ships WebGPU.
How do I convert GLSL shaders to WebGPU?
Port them to TSL (Three Shader Language), the node-based shader system of Three.js. You write TSL in JavaScript, and it compiles to WGSL on the WebGPU backend and to GLSL on the WebGL 2 backend, so one version covers both. ShaderMaterial, RawShaderMaterial, and onBeforeCompile patches don't run on WebGPURenderer at all, so every custom shader needs a port. The official GLSL-to-TSL transpiler example gives you a starting point.
How long does WebGPU migration take?
It depends on your custom code, not on the renderer swap. A project that uses only built-in materials mostly changes imports and initialization, then needs testing on both backends. Each ShaderMaterial, onBeforeCompile patch, and EffectComposer pass must be rewritten in TSL, and that is where most of the time goes. Run the audit in Step 1 to size the work before you commit to a schedule.

Technology-First Creative Studio


