Handbooks  /  Browser Internals
Handbook~16 min readFrontendworked math + runnable code
The Browser Internals Handbook

From tags
to pixels.

You ship HTML and CSS — a tree of tags and a pile of style rules — and somehow a browser turns it into a living, scrolling, animating picture at 60 frames a second. It does this through a render pipeline: parse, style, lay out, paint, composite. Most of what makes a page fast or janky comes down to one expensive stage — layout, the geometry math — and how often your code forces the browser to redo it. This handbook is that pipeline, why reflow costs what it costs, and the small rules that keep a page smooth.

01

The render pipeline

A browser turns your markup into pixels through a fixed sequence of stages called the render pipeline (or critical rendering path). It parses HTML into the DOM tree and CSS into the CSSOM, combines them into a render tree of everything visible, then runs layout to compute the exact position and size of every box, paint to fill in the pixels (text, colors, borders, shadows), and finally composite to stack the painted layers into the image you see.

The crucial fact is that the stages are ordered and dependent: each one feeds the next, so a change early in the pipeline forces everything downstream to run again. Change the text content and you may re-run layout, paint, and composite. Change only a color and you can skip layout and just repaint. Change only a transform and you can skip layout and paint, going straight to composite. That dependency chain is the entire performance story: the cost of a visual update is decided by how far back in the pipeline your change reaches. And the stage you most want to avoid re-running is layout.

The one-sentence version

The render pipeline is parse → style → layout → paint → composite, each stage depending on the last — so the cost of an update is how far back it reaches, and layout (reflow) is the expensive stage to avoid re-triggering.

02

Reflow is the tax

Layout — also called reflow — is where the browser computes the geometry of every element: where each box sits and how big it is. It's the pipeline's most expensive stage, and the reason is interdependence. Boxes affect each other: make one element wider and it can push its siblings, stretch its parent, and shift all their descendants. So a single geometric change can force the browser to recompute a whole subtree, at a cost that grows with the number of nodes it has to lay out.

Anything that touches geometry triggers a reflow: changing width, height, margin, padding, top/left, font-size, or adding/removing a DOM node. Changes that only affect appearance — color, background, visibility — skip layout and only repaint, which is much cheaper. This split is the single most useful thing to internalize about browser performance: geometric changes are expensive because they cascade; cosmetic changes are cheap because they don't. Every front-end optimization about "smooth scrolling" and "jank-free animation" is, at bottom, about not paying the reflow tax more often than you must.

03

Layout thrashing

The classic performance killer is layout thrashing: forcing the browser to reflow many times in a row when one would do. The browser is actually smart — it tries to batch your DOM changes and reflow once, at the end of the frame. You defeat that batching by interleaving writes and reads.

Batched vs thrashing (3 geometric writes)
BatchedWrite width, height, left → browser coalesces → 1 reflow at frame end.
Thrashingwrite → read offsetWidth → write → read → … forces a sync reflow each time → 3 reflows.
WhyReading a layout property needs an up-to-date answer → browser must flush a reflow now.
FixRead all layout values first, then do all writes → one reflow.

The trap is subtle because reading looks harmless. But when you read a layout property like offsetHeight after having written a geometric change, the browser can't give you a stale number — it must synchronously flush a reflow to compute the current value. Do that inside a loop (write, read, write, read…) and you trigger a reflow per iteration instead of one at the end. The fix is a discipline: batch reads and writes separately — gather all the measurements you need first, then apply all the mutations — so the browser coalesces everything into a single layout pass. Batching geometric updates is one of the highest-leverage optimizations in front-end code, and it's the direct payoff of understanding reflow.

04

The reflow math

Only geometric property changes force a reflow; a reflow's cost scales with the number of nodes it must lay out. Batched, many geometric writes collapse to one reflow; unbatched, each triggers its own:

reflows  =  1 if batched else (#geometric changes)     work  =  reflows × node_count

3 geometric writes over 100 nodes: batched = 1×100 = 100 units; thrashing = 3×100 = 300. Paint-only changes (color, background) = 0 reflows.

So the cost of a visual change depends entirely on which stage it reaches and how many times you re-enter it:

geometry (width, height, top…) → reflow     appearance (color, visibility) → repaint only     transform/opacity → composite only

The further right, the cheaper. The runnable version below classifies properties and computes reflow counts and total layout work, batched vs not.

RUN IT YOURSELF

Counting reflows

Browser performance comes down to the render pipeline. Changing a geometric property (width, height, position) forces a reflow — the browser recomputes layout for the affected nodes, at a cost that grows with node count. Changing a paint-only property (color, background) skips layout entirely. And the killer detail: batching geometric writes collapses them into one reflow, while interleaving reads and writes forces one reflow each — layout thrashing. Change the property list, node count, or batching flag and watch the reflow count and total layout work move.

CPython · WebAssembly
05

The fast path

To animate at a smooth 60 frames per second, the browser has ~16 milliseconds per frame to produce a new picture. Run layout and paint every frame and you'll blow that budget and drop frames (jank). The trick is to stay on the cheapest part of the pipeline — compositing — by animating only properties that don't need layout or paint:

What you changePipeline stages re-run · cost
width / height / top / leftLayout → Paint → Composite · expensive (reflow every frame — janky)
color / background / box-shadowPaint → Composite · medium (repaint, no layout)
transform / opacityComposite only · cheap (often GPU, its own layer — smooth 60fps)

So the practical rule for movement is: use transform: translate() instead of top/left, and opacity instead of changing visibility or background — because transform and opacity can be handled by the compositor on a separate layer, frequently on the GPU, without ever re-running layout or paint. That's why two animations that look like they do "the same thing" can feel utterly different: one takes the composite-only fast path and glides; the other reflows every frame and stutters. Combined with batching your reads and writes, staying on the composite path is most of what "web performance" means in practice. Understand the pipeline and the fix for a janky page is usually obvious: figure out which stage you're re-running every frame, and stop.

06

Pitfalls

The first is forced synchronous layout hidden in innocent code — a loop that sets a style and then reads offsetHeight/getBoundingClientRect() right after. Each read forces the browser to flush a reflow to answer accurately, so a loop over N elements can cause N reflows. It's the most common cause of scroll and animation jank, and it's invisible unless you know to look. The fix is the batching discipline: read all layout values in one pass, then write all mutations in the next.

Two more. Animating layout properties: transitioning width or left triggers reflow every frame and will never be smooth on a complex page — reach for transform instead, always. And giant DOM trees: since reflow cost grows with the number of nodes involved, a page with tens of thousands of DOM nodes reflows slowly no matter how careful you are; virtualize long lists (render only what's on screen) so layout works on a small tree. The unifying idea is that the browser is a pipeline with one expensive stage, and your job is to reach that stage as rarely as possible: prefer cosmetic changes to geometric ones, batch the geometric ones you can't avoid, and animate on the compositor. Learn the pipeline and web performance stops being folklore — it becomes a short, mechanical checklist about which stage a change re-runs and how often.

Worth knowing

Batch reads then writes to avoid forced synchronous layout, animate transform/opacity (composite-only) instead of width/top (reflow), and virtualize huge lists so layout works on a small tree. The whole game is re-running the layout stage as rarely as possible.

Frequently asked

Quick answers

How does a browser render a page?

Parse HTML→DOM and CSS→CSSOM, build a render tree, then layout (geometry) → paint (pixels) → composite (stack layers). Each stage feeds the next.

Why is reflow expensive?

Layout is interdependent — one box's size shifts others — so a geometric change can recompute a whole subtree, at cost growing with node count.

What is layout thrashing?

Interleaving DOM writes and layout reads forces a synchronous reflow each time. Fix: batch all reads, then all writes → one reflow.

How do you animate smoothly?

Animate transform and opacity — the compositor handles them (often on GPU) without layout or paint, so they hold 60fps.

Finished this one? 0 / 99 Handbooks done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.