Shagun Mishra
Sixteen milliseconds — how a browser draws a frame
↑↓ move · ↵ run · ? shortcuts
Keyboard
Sixteen milliseconds — how a browser draws a frame
01 — The budget
A 60 Hz display asks the browser a question every 16.7 milliseconds: what should I show now? Everything else on this page is a consequence of that deadline.
Miss it once and nobody notices. Miss it regularly and the page acquires that particular cheapness — scrolling that lags a finger behind, animation that stutters on the same frame every time. Users never diagnose it. They just decide the site feels bad.
Inside those 16.7 ms the browser runs five stages in a fixed order. Load any one of them too heavily and the frame is late.
Where the 16.7 milliseconds go
A rough shape, not a measurement — the split shifts with what your page does.
Blow the budget on purpose
This burns real CPU inside the animation callback, exactly where your own slow code would sit. Push it past 16 and watch the counter fall.
Frame time — · — fps · budget —
02 — Layout
Layout is the browser working out where every box goes. It is happy to do this once per frame, in a batch, at a moment of its choosing.
What it cannot do is answer a question about geometry while geometry is
dirty. Ask for offsetHeight after you have changed a style and the
browser must stop and lay out the document there and then. Do that in a loop
and you have layout thrashing — the same work, hundreds of times.
The demo below runs the identical 600 operations twice. The only difference is the order.
Same work, two orders
600 read/write pairs per pass
03 — Paint
Paint is rasterisation: taking geometry the browser now understands and filling in actual coloured pixels. Its cost scales with area, not with how many elements you have — one full-screen gradient can cost more than a thousand small icons.
The field below is a raster fill you control directly: every dot is pixels written into a canvas, so the count is the cost.
Move your cursor across the field
0 particles · — fps · — px filled / frame
04 — Composite
Once regions are painted they become layers, and the compositor's job is to stack and transform them. This is the cheap stage — a layer can be moved, scaled or faded without repainting a single pixel.
That is the whole reason transform and opacity
animate smoothly while top and height do not. One pair
is a compositor instruction; the other invalidates layout and paint.
The background of this page never touches layout or paint at all. It is a fluid simulation living entirely on the GPU — five shader passes per frame, composited straight to screen.
Solver parameters
Press ` for all twelve
05 — Text
Text is the most expensive thing most pages draw. Before a single pixel, the browser must shape it: map characters to glyphs, apply ligatures and kerning, and resolve the script's own rules. Only then does it rasterise, usually into a glyph atlas it can reuse.
Variable fonts change the arithmetic. Instead of shipping nine weight files the browser gets one outline plus axes, and interpolates. Fewer requests, less memory — but every distinct axis value is a new glyph to rasterise and cache.
Scroll and all four axes move together. Each card below locks the same letterform to one axis so you can isolate what it contributes.
Scroll progress 0% — drives every axis
font-variation-settings: …
06 — The other thread
Everything so far shares one main thread, and a long task on it delays the next frame. Audio does not get that luxury. A dropped frame is a flicker; a dropped audio buffer is an audible click, so Web Audio runs on its own real-time thread with its own deadline.
Your rendering code cannot block it and it cannot block your rendering code. The analyser below is the handoff point — the audio thread fills a buffer, and the main thread reads whatever is current when it happens to draw.
Nothing is recorded, stored or sent. The stream stops the moment you switch it off, and the browser's own indicator is the proof.
Loudest — · peak — · 0 bins
07 — Measure
None of this is worth memorising. What is worth building is the habit of looking — because the stage you assume is slow almost never is.
The counters below are live on this page, gathered with the same browser APIs you can use on yours.
This page, right now
Sampled continuously since load · 0 frames seen
// long tasks, in three lines, on any page
new PerformanceObserver(list => {
for (const e of list.getEntries()) console.log(e.duration.toFixed(1) + 'ms', e.name);
}).observe({type: 'longtask', buffered: true});
Change the load, watch the cost
Every control here alters how much work this page does per frame. Turn the fluid off and the GPU stage empties; raise dust and the raster cost climbs. The counters above respond.
Accent
Behaviour
Atmosphere
Saved to this browser only
Colophon
This page argues that the browser is more capable than we usually let it be, so it would be poor form to ship it through four layers of tooling. View source and you can read the whole thing: shaders as template strings, the solver, the map geometry, the audio graph.
The only request beyond the fonts is the hand-tracking model, and that is fetched lazily — never unless you ask for it.
| Composite | WebGL 2 | Ping-pong framebuffers at half resolution, HDR dye, ACES tone map |
|---|---|---|
| Simulation | Navier–Stokes | Semi-Lagrangian advection, Jacobi pressure solve, vorticity confinement |
| Paint | Canvas 2D | Particle field and spectrum bars, redrawn per frame |
| Text | Fraunces | Four variable axes driven by scroll position |
| Audio | Web Audio | 2048-point FFT on its own real-time thread |
| Input | MediaPipe | 21 hand landmarks from the webcam, loaded on request only |
| Measurement | PerformanceObserver | Long tasks, frame times and heap, sampled live |