# WebGPU frame time: the uploads you did not ask for

> Most of Threejs-Punk's speed-up was work the engine did unasked: instance matrices, dynamic buffers and TSL uniforms re-uploaded per draw. What changed.

Canonical page: https://andersonmancini.dev/blog/threejs-punk-fewer-uploads-faster-frames

Published: 2026-08-24 · 2 min read · Tags: three.js, webgpu, tsl, performance, game-dev

Most of the wins in [Threejs-Punk's](https://andersonmancini.dev/blog/threejs-punk-racing-game.md) frame budget were not "fewer effects". They were **work the engine did without being asked**: uploads per draw, render targets rendered once per reader, and a second scene render nobody needed.

## The numbers

Same harness, before and after, on an M2 Max in headless Chrome:

| | Before | After |
|--|--|--|
| 2880x1620, driving | 7.9 ms | 5.3 ms |
| 3840x2160, driving | 12.4 ms | 7.9 ms |
| CPU per frame | 7.4 ms | 2.4 ms |
| `queue.writeBuffer` per frame | 2,315 calls, 4.1 MB | about 240 calls, about 40 KB |
| Draw calls | about 475 | about 270 |

## Three kinds of waste

**Instance matrices.** An `InstancedMesh` with up to 1,024 instances keeps its matrices in a uniform buffer that is re-uploaded in full on every draw of every pass (view, mirror, shadow, rain height), even when nothing moved. Wrapping it so the matrices live in a **storage attribute** uploads only on `needsUpdate`, with update ranges. This works on the WebGPU backend only.

**`DynamicDrawUsage`.** three re-uploads a dynamic attribute on every use. Attributes that already flag `needsUpdate` must stay static usage.

**The shared render group.** Camera, lights and shadow uniforms live in one buffer shared by every material with the same uniform set. A uniform created *inside* a TSL `Fn` body is a new node for every material using that `Fn`, and that one private node splits the group per material. three's PCF-soft shadow filter did exactly that, giving around 390 render-group buffers rewritten every frame. The fix is the same filter with the uniform created once. The rule: create render-group uniforms outside `Fn` bodies.

## Two more

- **Render targets that render once.** An `rtt()` node re-renders for every pass that reads it. When more than one pass reads it, set its update type to once per frame and sample the texture.
- **Images, not video.** Six `VideoTexture` ad screens became posters on one GPU-compressed atlas with a slideshow in the shader.

## How to find them

Switching one subsystem off and comparing frame time beats reading pass timings. The measuring method is in [the GPU timing post](https://andersonmancini.dev/blog/threejs-punk-measuring-gpu-time-tile-based.md). The game is on its [project page](https://andersonmancini.dev/projects/threejs-punk.md).

## Related projects

- [Threejs-Punk](https://andersonmancini.dev/projects/threejs-punk.md) — Fully featured Racing Game
