Cutting a WebGPU game's cold start from 29 to 14 seconds
· 2 min read · webgpu, three.js, tsl, performance, game-dev
A WebGPU game compiles shaders at the start, and a first visit has a cold driver cache. For Threejs-Punk, the loader's shader phase built 1,016 pipelines from 829 shader modules in 6.3 s, 69% of it JavaScript building TSL node graphs, only 16% waiting for the GPU. Here is what was wrong.
1. The warm-up compiled for the wrong target #
compileAsync(scene, camera) builds for the renderer's current target, while the scene was drawn by a beauty pass into its own MRT. So 478 warm-up pipelines were never used, and the real ones were built synchronously on the first frames. The fix sets the pass's own target and MRT for the compile.
2. Render contexts are keyed by call depth #
The beauty pass draws nested inside the post chain, at depth 2, or 4 with extra effects, and three keys render contexts, hence pipelines, on that depth. The whole scene was built once per depth. Pinning every depth to one context fixed it.
3. Every InstancedMesh is its own shader #
three puts the object's uuid in the cache key and names the instance buffer by node id, so 565 modules were the same vertex shader under another buffer name. The fix is to hide the (asset, LOD) meshes that never draw, so nothing builds them.
The second round #
Two kinds of pipelines were still made synchronously on the render path, where the GPU process compiles them one after another, while async ones compile about 5 times in parallel. A dry run of the warm-up's own render calls with pipelines forced async, awaited in flight, made them in parallel. The car's eight heavy PBR materials had been compiled on the first loop frame because compile culls by frustum, so the compile now lifts frustum culling.
Results #
Production build, headless Chrome, median of three runs:
- 1,016 to 432 pipelines, 829 to 512 modules,
- "compiling shaders" 6.3 to 3.5 s in the first round,
- cold first visit ready 28.9 to 14.0 s after the second round,
- pipelines built after pressing ENTER: 124 to 8.
These lean on three internals, so they have to be re-checked on every three upgrade. The phone side of the same problem is in the iPhone shader count post. The project is on its project page.
Related projects
- Threejs-Punk — Fully featured Racing Game