Command Palette

Search for a command to run...

Hectal
Course outline
Topic 13.1Advanced ~21 min · step 1 of 6

WebGPURenderer and Its Fallback

In one line

WebGPURenderer from three/webgpu uses WebGPU when the browser has it and WebGL 2 when it does not, with the same code. It starts asynchronously, so you await renderer.init() before the first frame and read renderer.backend to see what you got.

0/6 · 0%

Think of it like this

A restaurant has a new kitchen with better equipment, but some days the new ovens are not ready. A good head chef checks the kitchen first, then uses the new ovens if they work and the old ones if they do not. The customers get the same menu either way. WebGPURenderer is that chef: it checks for WebGPU, and falls back to WebGL 2 without you changing your scene code.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

WebGPU
A newer browser API for the graphics chip. It is explicit and modern, and it supports compute shaders.
backend
The layer that talks to the browser's graphics API. WebGPURenderer has a WebGPU backend and a WebGL 2 backend.
fallback
The plan B that runs when plan A is missing. Here, WebGL 2 runs when WebGPU is not available.
adapter
The browser's handle for one physical or software GPU. navigator.gpu.requestAdapter() returns one, or null.
init()
The async method that chooses the backend and creates the GPU device. Nothing can be drawn until it finishes.
output pass
The last small draw that copies the frame to the canvas, applying tone mapping and colour space.

Step by step

01Import from three/webgpu

import * as THREE from "three/webgpu" replaces from "three". It carries the same classes plus the new renderer. If you only need a few names, import { WebGPURenderer, Scene } from "three/webgpu" works the same.

02Create, then await init

new THREE.WebGPURenderer({ antialias: true }) creates the object at once, with nothing chosen yet. await renderer.init() is where the browser is asked. Top-level await works in a module, as here and in Vite.

03Read the backend

After init(), renderer.backend.isWebGPUBackend tells which one is in use. Log it once at start-up and put it in bug reports: "it looks wrong" means two different things on two backends.

04Draw and count

The rest is the code you know: scene, camera, setAnimationLoop, render. The first example prints one frame's draw calls (2) and triangles (13): the cube's 12, plus the output pass's 1.

05Plan for the missing case

Nothing extra is needed for a browser without WebGPU: the fallback is automatic. What you must plan for is the features that only WebGPU has, such as workgroup memory in compute (Topic 13.5), and the visitor without WebGL 2 either, which Phase 1's support check already handles.

Plan for the missing casediagram
Rendering diagram…

Try it yourself

  1. 1

    Force WebGL 2

    In the first example change the constructor to new THREE.WebGPURenderer({ antialias: true, forceWebGL: true }). The console prints backend: WebGL 2 (fallback) and no warning line. The label says "fallback" because the code only tests isWebGPUBackend: a forced WebGL 2 is not a fallback, so check what you passed if the difference matters.

  2. 2

    Use a bigger scene

    Change BoxGeometry(1, 1, 1) to SphereGeometry(0.8, 64, 32). The triangle count in the console becomes 3,969: the sphere's 3,968 triangles plus the output pass's 1. Draw calls stay at 2.

Code & diagrams

Start, init, and ask which backend r186+ New tab

No `output` is set because the line differs by browser. In a browser with WebGPU it prints `backend: WebGPU` and no warning. In the checker's browser it prints the fallback warning and `backend: WebGL 2 (fallback)`. Look at the status bar under the canvas: it names the backend too.

idle

Console

Run the scene to see its console.
Drawing before init, and forcing WebGL 2 New tab

`forceWebGL: true` gives the same result in every browser, so this example has a fixed output. The error text is three.js's own.

idle

Console

Run the scene to see its console.

Expected console output

too early: THREE.Renderer: .render() called before the backend is initialized. Use "await renderer.init();" before rendering.
backend: WebGL 2
first frame: draw calls 2 | triangles 13
A browser without WebGPU (Firefox on Linux, for example) New tab

The example hides `navigator.gpu` to act like such a browser. The renderer does not throw: it warns and switches. The output is the same in every browser here, including the warning line.

idle

Console

Run the scene to see its console.

Expected console output

navigator.gpu: undefined
THREE.WebGPURenderer: WebGPU is not available, running under WebGL2 backend.
backend: WebGL 2 (fallback)

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget await renderer.init()

In the first example delete the await renderer.init(); line.

terminal
$ Run
── what you'll see ──
In the checker's browser (no WebGPU):
navigator.gpu exists in this browser: true
backend: WebGPU
[warn] THREE.WebGPURenderer: WebGPU is not available, running under WebGL2 backend.
one frame: draw calls 2 | triangles 13

Break #2

Use the WebGL renderer name

In the first example change new THREE.WebGPURenderer( to new THREE.WebGLRenderer(.

terminal
$ Run
── what you'll see ──
Uncaught TypeError: THREE.WebGLRenderer is not a constructor

Myth vs fact

Myth

WebGPURenderer needs WebGPU, so I must keep a second code path for the other browsers.

Fact

It has the WebGL 2 path built in. You keep one scene. You only need extra code for features that exist on WebGPU alone, such as compute with shared memory.

Myth

WebGPU is always faster.

Fact

Not for every scene. A simple scene can run the same on both, and the WebGPU renderer starts slower and downloads more. Measure your own scene on both backends with the status bar.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    three.js requests every optional feature the adapter offers, but no extra limits. A limit such as maxComputeInvocationsPerWorkgroup therefore stays at the WebGPU default (256), below what the hardware can do. To get more, pass requiredLimits in the renderer options (the request fails if the adapter cannot grant it).

  • ▸

    If the GPU device is lost while the page runs (a driver reset, for example), WebGPURenderer stops drawing, and render() and compute calls return without doing anything. It does not recover by itself: the hook is renderer.onDeviceLost. Topic 18.2 covers loss and recovery.

  • ▸

    renderer.debug.getShaderAsync(scene, camera, object) returns the exact vertex and fragment shader text three.js made for an object, in WGSL or GLSL depending on the backend. Topic 13.3 uses it to read what TSL becomes.

Remember this

  1. 1

    The problem this solves: WebGL 2 follows OpenGL ES 3.0 (2012), an older design that cannot do everything modern ones can, such as general-purpose compute. WebGPU can. But WebGPU is not in every browser. If you wrote only for WebGPU, some visitors would see nothing. One renderer that covers both lets you move forward without dropping them.

  2. 2

    Where it lives. import * as THREE from "three/webgpu" gives you everything in three plus WebGPURenderer and the node materials. Classes are shared with "three": a Mesh from either import is the same class, because both entry points import one shared core file, so your other modules, OrbitControls and the loaders keep working. The exception is WebGLRenderer, which three/webgpu does not export: new THREE.WebGLRenderer() there throws THREE.WebGLRenderer is not a constructor.

  3. 3

    It starts asynchronously. Asking the browser for a GPU takes time, so the renderer cannot be used the moment you create it. await renderer.init() picks the backend and creates the device. Call render() before it finishes and three.js throws: THREE.Renderer: .render() called before the backend is initialized. Use "await renderer.init();" before rendering. setAnimationLoop is more forgiving, but wait for init() anyway when you want to read renderer.backend.

  4. 4

    Which backend am I on? After init(), renderer.backend.isWebGPUBackend is true for WebGPU. When WebGPU cannot start (no navigator.gpu, no adapter, a blocklisted driver), three.js logs the warning THREE.WebGPURenderer: WebGPU is not available, running under WebGL2 backend. and uses its WebGL 2 backend. new WebGPURenderer({ forceWebGL: true }) skips WebGPU on purpose, which is useful for testing the fallback and for comparing.

  5. 5

    What is different when you measure. renderer.info.render.drawCalls and triangles describe the current frame, but info.render.calls counts render() calls since the start, not draw calls as in WebGLRenderer. A frame with one cube shows 2 draw calls and 13 triangles. The extra call is a full-screen triangle: the scene is drawn into a hidden texture, and this triangle copies it to the canvas while applying tone mapping and the output colour space.

  6. 6

    What it costs. The smallest scene built with Vite 8.3.4 is 524.77 kB of JavaScript (130.25 kB gzip) with WebGLRenderer and 781.97 kB (212.90 kB gzip) with WebGPURenderer, measured for this course. Start-up takes longer too, because the device and pipelines are created asynchronously. Choose it for what it gives you: node materials, TSL and compute.

Explain it without notes

01

Why must you await renderer.init() with WebGPURenderer, and what happens if you do not?

02

A visitor reports the scene looks different in Firefox on Linux. What do you check first?

Practice

01

Write a function createRenderer(container) that returns a started WebGPURenderer and logs the backend name. What must it be?

02

Using the first example, make a version that stays on WebGL 2 when the URL contains ?gl=1.

Trade-offs

  • ↔

    Pick WebGPURenderer when you want TSL, node materials or compute. Pick WebGLRenderer when you rely on ShaderMaterial, onBeforeCompile or EffectComposer, or when the smaller download matters more.

Done when you can

  • Start a WebGPURenderer with await renderer.init() and read which backend it chose

  • Explain the fallback warning and when you see it

  • Force WebGL 2 with forceWebGL to test the fallback

  • Read info.render.drawCalls and explain why one cube shows 2