Core Idea
Drag the DPR lever above: the window never changes size, yet the shaded-pixel count scales with DPR² and the frame time follows — that single lever is most of this page. The diagnosis method lives on the CPU/GPU-bound page; this page is what to do when the verdict is fill-bound on a phone — the most common mobile verdict by far. Phones stack the deck against pixels: high-density screens mean a DPR-3 device pushes 9× the pixels of DPR-1 at the same CSS size, through a GPU that shares memory bandwidth with everything else and throttles itself as it heats. Desktop habits that cost nothing become the whole frame.
The DPR Cap — the Biggest Single Lever
Rendering resolution is CSS size × device pixel ratio, and capping it is one line:
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5))
Dropping a DPR-3 phone to 1.5 quarters the shaded pixels — multiplied again by every full-screen post pass (bloom alone re-shades the screen several times). The tradeoff is purely aesthetic softness, barely visible on anything in motion. It also doubles as a diagnostic: if the cap barely moves the frame time, the wall isn’t fill — it’s vertices, and the next lever is LOD.
Overdraw — the Silent Killer
Fill cost isn’t just how many pixels — it’s how many times each one is shaded. Opaque geometry gets a discount (the depth test kills hidden fragments early), but transparent layers can’t use it: every blended pane pays full price for every pixel it covers.
Stack the panes and flip to the heatmap: the terracotta zones are being shaded four times and more — and a phone pays that bill sixty times a second. This is why particle clouds, UI layers, and foliage cards (many overlapping transparent quads) are mobile’s classic fill bombs. The discipline: fewer and smaller transparent surfaces, tighter sprite shapes over big soft quads, and opaque wherever opaque works.
The Rest of the Lever Board
In rough order of return, after the cap:
| Lever | Buys back |
|---|---|
| material tier downgrade (PBR → Lambert where realism isn’t needed) | per-pixel cost — often a ~2× mobile win (shading, Three.js tiers) |
| post-processing off or cheaper | whole-screen passes, each a full repaint |
| fewer/simpler lights, baked or no shadows | per-pixel lighting work |
| texture sizes matched to screen coverage | memory bandwidth (mipmaps handle distance) |
CPU-side levers — draw-call batching, allocation-free loops (garbage collection pauses read as stutter, not slowness) — are their own pages; they matter on mobile too, but they fix a different wall.
One Mobile-Only Trap: Deferred First Draws
A hitch that appears only on phones: a stutter the first time an object type is actually drawn. Mobile drivers defer shader finalization and geometry/texture upload until the first real draw call — compile() and upload hints only start the work. The fix is a preheat pass: render everything once, visible and unculled, behind the loading screen, so the driver commits shaders and uploads while nobody’s watching. Desktop bandwidth absorbs this invisibly; phones don’t.
The Discipline
Every fix ends the same way: re-measure. The bottleneck moves — cut fill and the frame may become vertex-bound; cut calls and it becomes fill-bound. Mobile optimization isn’t a checklist run once, it’s the two-halves diagnosis run after every change.
Connections
This page manages the pixel half of the GPU pipeline’s bill — the fragment work that shading defines and texturing feeds. Its sibling levers live on draw-call batching (the CPU wall) and level of detail (the vertex wall), the stutter that isn’t a wall at all belongs to garbage collection, and the whole discipline hangs off CPU-vs-GPU-bound’s frame-budget instrument.