WebGPU in 2026: porting a Three.js game from WebGL to WebGPU

WebGPU has been "almost ready" for years. In 2026 it's actually ready — Chrome, Edge, Firefox 130+, and Safari 18.2 all ship stable support, including on mobile. Three.js made the switch painless starting with r171: you change one import line and the renderer swaps out. But when you're porting a game — with custom shaders, thousands of instanced objects, and a particle system that was already fighting the garbage collector — the one-liner is just the first step. This post is a practical walkthrough of the migration, the gotchas I hit, and the performance wins that actually showed up in the profiler.

The one-line swap (and what it gives you for free)

The official migration path is embarrassingly simple. You swap your import from the default Three.js entry point to the WebGPU bundle:

// Before — WebGL
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });

// After — WebGPU
import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init();   // ← this is new and mandatory

That last line matters. WebGPURenderer initialization is asynchronous — it has to request a GPU adapter and device before it can do anything. If you call renderer.render() before init() resolves, you get a silent black canvas. In a game with an requestAnimationFrame loop that fires immediately, this means restructuring your boot sequence so the first frame waits for the renderer. I wrapped mine in an async init() function that calls await renderer.init() before entering the loop — took about two minutes to wire up.

Everything built into Three.js — MeshStandardMaterial, MeshPhysicalMaterial, lights, shadows, post-processing via the new PostProcessing class — works out of the box. If your game only uses stock materials and the built-in fog/shadow pipeline, you may genuinely be done after the import swap and the async init. For a simple demo scene I tested, it rendered identically frame-one with zero other changes.

The three/webgpu entry point also includes a silent WebGL 2 fallback. If the browser doesn't support WebGPU (rare now, but possible on older devices), it drops to WebGL 2 automatically. You don't have to write any detection logic yourself.

Shader migration: GLSL → TSL

The one-liner doesn't help if you have custom shaders. ShaderMaterial and RawShaderMaterial are GLSL-only and are ignored by the WebGPU backend — they still work if the renderer falls back to WebGL, but on WebGPU they just won't render.

Three.js's answer is TSL — Three Shader Language. It's a JavaScript-level API for writing shaders as node graphs that Three.js compiles to either GLSL or WGSL depending on the active backend. Write once, run on both. The syntax feels like writing math in JavaScript rather than writing GLSL strings:

// GLSL (old)
uniform float uTime;
varying vec2 vUv;
void main() {
  float wave = sin(vUv.x * 10.0 + uTime) * 0.1;
  gl_Position = projectionMatrix * modelViewMatrix
              * vec4(position + normal * wave, 1.0);
}

// TSL (new)
import { uniform, sin, uv, positionLocal,
         normalLocal, mul, add } from 'three/tsl';
const uTime = uniform(0.0);
const wave  = sin(add(mul(uv().x, 10.0), uTime)).mul(0.1);
material.positionNode = add(positionLocal, mul(normalLocal, wave));

It looks more verbose than GLSL, but you get two things for free: the shader runs on both WebGL and WebGPU, and you can compose nodes programmatically. Need ten variants of the same wave shader with different frequencies? That's a loop in JavaScript generating node trees, not ten hand-maintained GLSL files.

The migration cost depends entirely on how many custom shaders you have. A game with one or two effects takes an afternoon. A game with a full custom pipeline — deferred rendering, screen-space reflections, custom tone mapping — is a bigger project. In my case I had three custom shaders (a terrain displacement, a muzzle-flash glow, and a scope overlay) and converting them to TSL took roughly a day.

WebGL Pipeline vs WebGPU Pipeline in Three.js WebGL (old) WebGLRenderer Sync init ShaderMaterial GLSL strings Draw calls CPU-bound batching Particles CPU update loop No compute WebGPU (new) WebGPURenderer Async init TSL Nodes JS → WGSL / GLSL Draw calls Lower CPU overhead Particles GPU compute Compute shaders ✓ Green = new capability or improvement · Grey = WebGL limitation The renderer swap is one line; the real wins come from compute shaders and lower draw-call overhead.
The WebGL → WebGPU pipeline in Three.js. Stock materials port automatically; custom shaders need TSL. Compute shaders are the biggest unlock.

Compute shaders: the actual game-changer

The performance win from swapping the renderer alone was modest in my tests — maybe 5–10% fewer milliseconds per frame on draw-call-heavy scenes, which is consistent with what Babylon.js and others have reported. Nice, but not transformative.

The transformative part is compute shaders. WebGL doesn't have them. WebGPU does, and Three.js exposes them through TSL. The concept: instead of running a vertex-and-fragment pipeline that draws pixels, a compute shader runs arbitrary parallel work on the GPU and writes results into buffers. Physics, particle updates, spatial hashing, pathfinding — anything that's embarrassingly parallel and was previously burning CPU cycles in your requestAnimationFrame callback.

The pattern in Three.js looks like this:

import { instancedArray, wgslFn, compute } from 'three/tsl';

// Create a GPU-side buffer for 10,000 particle positions
const positions = instancedArray(10000, 'vec3');

// Define the compute kernel in WGSL
const updateKernel = wgslFn(`
  fn main(@builtin(global_invocation_id) id: vec3u) {
    let i = id.x;
    var pos = positions[i];
    pos.y -= 0.02;                  // gravity
    if (pos.y < 0.0) { pos.y = 5.0; } // respawn
    positions[i] = pos;
  }
`);

// Dispatch 10,000 threads — runs every frame on the GPU
const computeNode = updateKernel({ positions }).compute(10000);

// In your render loop:
renderer.compute(computeNode);
renderer.render(scene, camera);

I moved a muzzle-flash particle system — 5,000 particles that were being updated in a for loop on the CPU every frame — to a compute shader. The CPU time for that system dropped from about 2.8 ms per frame to effectively zero (the GPU runs it alongside the render pass). On a mid-range laptop that was the difference between a solid 60 fps and occasional drops to 45.

Instanced meshes get a compute-powered upgrade

In WebGL, instanced meshes are already the standard way to draw many identical objects (bullets, foliage, debris). You set up an InstancedMesh with a count and update each instance's transform matrix from JavaScript every frame. It works, but the CPU update loop scales linearly — 10,000 instances means 10,000 matrix writes on the main thread.

With WebGPU, you can write instance positions directly from a compute shader. The instancedArray function in TSL creates a GPU buffer, the compute kernel updates it in parallel, and the instanced draw reads from the same buffer. The CPU never touches the per-instance data at all. For a debris field with 10k fragments, this cut the per-frame update from ~4 ms on the CPU to a sub-millisecond GPU dispatch.

Post-processing: the new API

If you were using EffectComposer from three/examples/jsm/postprocessing, you'll need to switch to the new PostProcessing class that ships with the WebGPU bundle. The old EffectComposer was built on WebGL framebuffers and render targets; the new one uses TSL pass nodes:

import { PostProcessing } from 'three/webgpu';
import { pass, bloom } from 'three/tsl';

const postProcessing = new PostProcessing(renderer);
const scenePass = pass(scene, camera);
const bloomPass = bloom(scenePass, {
  threshold: 0.85,
  intensity: 1.2,
  radius: 0.4,
});
postProcessing.outputNode = bloomPass;

// In render loop: postProcessing.render() instead of renderer.render()

Most common effects — bloom, SSAO, tone mapping, FXAA — have TSL equivalents. If you had a custom post-processing pass, it needs to be rewritten as a TSL node, which is the same amount of work as the shader migration above.

Browser support: where it actually stands

As of mid-2026, WebGPU is shipped and stable in Chrome (desktop + Android), Edge, Safari 18.2 (macOS + iOS), and Firefox 130+. The "is it ready?" question from 2024 is settled — the answer is yes for any project that can also tolerate a WebGL 2 fallback on the long tail of older devices. The three/webgpu import handles the fallback automatically, so you don't have to branch your code.

The one caveat: compute shaders require WebGPU. If a user's browser falls back to WebGL 2, compute-dependent features (GPU particles, GPU physics) won't run. You need either a CPU fallback path or to accept that those features are WebGPU-only. For a game, my approach was to keep a simple CPU particle path as a fallback and only enable the compute version when navigator.gpu is available.

Is it worth porting now?

If your game uses only stock materials and the built-in pipeline — yes, unconditionally. The port is a five-minute import swap that gives you lower draw-call overhead, automatic WebGL fallback, and future access to compute shaders when you want them.

If you have a lot of custom GLSL — the calculus is different. TSL migration is real work, and GLSL shaders still function fine on WebGL 2, which runs everywhere. The reason to port is if you need compute shaders (for GPU particles, physics, or spatial queries that are currently bottlenecking the CPU) or if you want a single shader codebase that targets both backends going forward.

For me, the biggest practical win wasn't the renderer swap itself — it was compute shaders for particle systems and instanced-mesh updates. That moved real milliseconds off the CPU and made 60 fps feel effortless on hardware that was previously borderline. If your game is CPU-bound on per-object updates, that's the feature worth porting for.

Related
→ Building real-time multiplayer games with Supabase Realtime → Three.js WebGPURenderer docs → Maxime Heckel's field guide to TSL and WebGPU
Can I use WebGPURenderer in Three.js today?

Yes. Since Three.js r171 (September 2025), WebGPU is production-ready with a simple import change. Use import * as THREE from 'three/webgpu' instead of the default entry point. It includes a built-in fallback to WebGL 2 for browsers that don't support WebGPU yet.

Do I have to rewrite all my GLSL shaders for WebGPU?

Not necessarily. Built-in Three.js materials (MeshStandardMaterial, MeshPhysicalMaterial, etc.) work with WebGPURenderer without changes. Only custom ShaderMaterial or RawShaderMaterial code needs conversion — either to TSL (Three Shader Language), which targets both backends, or to raw WGSL if you only need WebGPU.

Is WebGPU faster than WebGL for Three.js games?

It depends on the workload. For draw-call-heavy scenes (many objects, particles, instanced meshes), WebGPU can be significantly faster because of lower CPU overhead per draw call. For simple scenes with few objects, you may not see much difference. The biggest unlock is compute shaders, which let you offload physics, particle simulation, and spatial queries to the GPU — work that previously had to run on the CPU in WebGL.