Core Idea
Every frame is produced by two different machines working together. The CPU decides what to draw and where; the GPU actually paints it. The frame is only as fast as the slower half — drag either bar past the other and watch the fix for one become useless:
You are CPU-bound when the CPU half is the wall, GPU-bound when the GPU half is. The tool above measures both halves of a real scene live: drag the levers and watch the bound move.
Two Different Shapes of Machine
| CPU | GPU | |
|---|---|---|
| Cores | a few, very fast | thousands, individually slow |
| Built for | sequential, branchy logic | the same tiny program on millions of items |
| In a frame | game logic, then one draw-this order per object | transform every vertex, shade every pixel |
| In the browser | everything on the single JS main thread | vertex → rasterize → fragment → post passes |
They are not “fast” and “faster” — they are different shapes. Give each the work it’s shaped for.
The Deadline
60 fps means each frame must finish in 16.67 ms. With vsync there is no partial credit: a frame that misses slips to the next refresh, snapping straight toward 33 ms / 30 fps — there is no in-between. The meter above draws the deadline as a line; push a lever past it and watch the bars jump to the next plateau rather than climb smoothly.
The CPU’s Real Cost: Orchestration, Not Computation
The logic itself is almost never the wall — a modern CPU does billions of operations per second, so updating hundreds of animated objects costs microseconds. The expensive part is talking to the GPU: every draw call is a trip across the JS → driver → GPU boundary, validated and serialized on one thread.
The CPU is a manager; the GPU is a factory floor of a thousand workers who finish instantly and stand idle, waiting for the next order. Watch the floor — then switch to one batched order:
The manager is the bottleneck, not the work. The fix is fewer, bigger orders: draw-call batching (instancing).
Which Half Are You In?
| Observation | Diagnosis |
|---|---|
| Main thread busy the whole frame (DevTools Performance) | CPU-bound |
| Main thread short, frame still long | GPU-bound |
| Cap DPR → frame time drops a lot | you were fill-bound |
| Cap DPR → barely moves | CPU- or vertex-bound |
The readout in the tool above shows the same split: CPU half (submit) is the real time spent issuing commands on the JS thread, measured against the whole frame.
Why Mobile Falls Off a Cliff
The two costs that scale worst from desktop to mobile are per-draw-call overhead (weaker cores, heavier drivers) and fill rate — triangle count usually survives the trip. A scene comfortably at 38 fps on a desktop can collapse to 13 fps on a mid-range phone. A healthy mobile budget is roughly 100–200 draw calls.
You don’t optimize what’s slow on your dev machine — you optimize what will be slow on the target. If the tool above stays green with every lever maxed, that is this lesson, live.
Connections
CPU-bound is almost always a draw-call problem → draw-call batching (instancing, geometry merging). GPU-bound is a fill/vertex problem → mobile optimization (DPR cap, cheaper materials, LOD). The split mirrors the hardware stages of the GPU pipeline. Frame stutter (spikes, not steady cost) is often garbage collection — a different disease with a different cure.