The Rendering Pipeline: From DOM to Pixels (Style, Layout, Paint, Composite)
In the past few articles we assembled two trees: the DOM (content) and the CSSOM (styles). We also worked out that rendering is just one step of the event loop, slotted between tasks.
But the screen is still empty. This article takes the last step: how does the browser turn those two trees into the pixels you see?
Along the way we'll settle an old friend: why is changing width janky while changing transform is smooth? The answer hides inside this pipeline.
What You'll Learn
- The four stages of one frame: style → layout → paint → composite
- The difference between a "reflow," a "repaint," and a "composite-only" change — and their costs
- Why
transform/opacityare cheap whilewidth/top/font-sizeare expensive - Layers and the compositor thread, and why scrolling can stay smooth
- The most common performance trap: layout thrashing
- A practical optimization checklist
Prerequisites
- Rendering is one step of the event loop:
- Parsing is tasks, and rendering slots between tasks:
- Where DOM and CSSOM come from:
Starting from a Scenario
The same "double the width" animation, written two ways:
/* Version A: change width */
.box { width: 100px; transition: width .3s; }
.box:hover { width: 200px; }
/* Version B: change transform */
.box { width: 100px; transition: transform .3s; }
.box:hover { transform: scaleX(2); }A is janky; B is smooth. Both "become twice as wide" — why such a difference? To answer that, we first need to see how one frame is manufactured.
Core Content
1. The Pipeline: Four Stages of a Frame
When "something on the page changes," the browser has to produce a new frame, going roughly through four stages:
style → layout → paint → composite
(what it looks (where it is, (how to draw it (stitch layers
like) how big) into pixels) into the image)- Style: match CSS rules to elements and compute each element's final style.
- Layout: from those styles, compute each element's position and size on the page.
- Paint: describe elements as "how to draw them" (color, text, shadow…), producing draw commands.
- Composite: stitch the layers into the final image and hand it to the GPU.
The single most important point: you don't always re-run all four stages. The property you change decides which stage the work restarts from — and the earlier the stage, the higher the cost. This is the master key to everything below.
2. Style
- What it does: match CSS rules to elements, handle inheritance, the cascade, and specificity, and compute each element's "computed style."
- When it runs: DOM changes, class changes, CSSOM changes…
- In DevTools it's called Recalculate Style.
This stage is usually cheap, but gets slower with overly complex selectors and huge numbers of rules.
3. Layout (Reflow)
Layout computes, from each element's computed style, its geometry — position (x, y) and size (width, height).
Why is it the most expensive? Because layout is interdependent:
you widen a div → the element next to it gets pushed aside → maybe the parent
grows → which affects siblings and descendants… a chain reactionSo as soon as geometry changes ( width, height, margin, padding, top, left, font-size… ), it can trigger a large-scale reflow that ripples through the render tree. That's why "changing width is janky."
This stage is also called reflow or layout — the same thing.
Layout thrashing — covered later — is the most common trap here.
4. Paint (Repaint)
Once layout has fixed positions and sizes, the paint stage translates each element into "how to draw it": what color to fill, what text to render, whether there's a shadow, how thick the border is… outputting a set of draw commands.
- Changing geometry-free properties (
color,background,box-shadow,border-color…) → repaint only, no reflow. - A repaint is lighter than a reflow, but a large repaint area is still costly.
- DevTools' Paint flashing highlights the repainted regions.
5. Composite
Here's a problem: if the whole page were a single big bitmap, then even a tiny corner change would mean re-uploading the entire image — way too expensive.
So the browser splits the page into multiple layers. When something changes, only the affected layers are repainted, and then the compositor stitches the layers together for the screen.
What's special about compositing: it only does "layer jigsaw," and never touches layout or paint. So when a change only needs compositing, the cost is tiny — it can even be handed to the GPU.
And transform and opacity happen to be able to trigger compositing only (as long as the element is on its own layer). That's the real reason they're "smooth."
Aside:
will-change: transformortranslateZ(0)can hint the browser to promote an element to its own layer ahead of time. But don't overuse it — layers eat memory.
6. Three Kinds of Cost: Reflow vs. Repaint vs. Composite-Only
Let's close the loop on the opening question:
| Property you change | Which stage restarts | Cost |
|---|---|---|
width / height / margin / padding / top / left / font-size | Layout (reflow) | Highest; can ripple widely |
color / background / box-shadow / border-color | Paint (repaint) | Medium |
transform / opacity | Composite (composite-only) | Lowest; can go to the GPU |
So "changing width is janky" because it triggers a reflow; "changing transform is smooth" because it triggers compositing only. The gap isn't "widen vs. scale" — it's "which stage of the pipeline restarts."
7. The Compositor Thread: Why Scroll and transform Animations Stay Smooth
Recall the process/thread article: inside the renderer process there's a compositor thread.
Scrolling, and transform / opacity animations, can be handled on that compositor thread without going through the main thread. So:
- Even if the main thread is blocked by JS, scrolling and such animations may still be smooth;
- But the moment an animation property triggers layout or paint, it has to go back to the main thread → block the main thread and the animation janks.
This also explains "why the page freezes during while(true){} but some scrolling still works" — if that scroll can be handed to the compositor, it isn't affected by the main thread. (If a scroll handler bound to the main thread is involved, that's a different story.)
8. Layout Thrashing: The Easiest Trap to Fall Into
Look at this innocent-looking code:
for (const el of elements) {
const h = el.offsetHeight; // read: forces an immediate reflow
el.style.width = h * 2 + "px"; // write: dirties layout again
}Reading layout properties (offsetHeight, offsetTop, getBoundingClientRect(), and so on) forces the browser to complete a layout synchronously, right now, because you need that value immediately. So "read → write → read → write" alternation turns into repeated forced reflows, and performance collapses.
The fix: separate reads and writes — read them all first, then write them all.
const heights = elements.map((el) => el.offsetHeight); // read them all first
elements.forEach((el, i) => (el.style.width = heights[i] * 2 + "px")); // then write(For complex cases, use requestAnimationFrame to batch the writes just before the next frame.)
9. A Developer's Optimization Checklist
- Prefer
transform/opacityfor animations (composite-only). - Avoid layout thrashing: batch reads, batch writes, don't alternate.
- Use
will-changewith care: only on elements that genuinely animate frequently. - Shrink the reflow scope: take frequently-changing elements out of normal flow (
position: absolute/fixed) so their geometry changes don't ripple to others. - Use DevTools: turn on Paint flashing, the FPS meter, and Layout Shift Regions in the Rendering panel; watch Layout / Paint / Composite timings in the Performance panel.
Deeper Understanding (Common Misconceptions)
- "
transformis cheap because it doesn't take up space." No. It's cheap because it triggers compositing only, skipping layout and paint. - "A repaint is always lighter than a reflow." Usually, but a large repaint is expensive too.
- "The more
will-change, the better." The opposite. Every layer costs memory; overusing it backfires. - The FLIP technique: to make even a "layout change" smooth, do First (record the initial position) → Last (move to the end state) → Invert (offset it back with
transform) → Play (transition totransform: none). This converts a "reflow animation" into a "composite-only animation." - "The rendering pipeline always runs all four stages." No. The property you change chooses the starting stage, and the earlier it is, the more expensive.
Hands-On Practice
-
Paint flashing comparison Open DevTools → Rendering → enable Paint flashing. Change an element's
colorand then itswidth, and watch which regions get highlighted (the repaint area). -
Manufacture layout thrashing yourself Write a loop alternating
obj.offsetHeightandobj.style.width, record it in the Performance panel, and look for the "forced reflow" warnings. -
See the whole pipeline Record an animation in the Performance panel and compare the Layout / Paint / Composite timings of a
widthanimation versus atransformanimation. -
A small FLIP experiment Rewrite a "position change" animation using FLIP and feel the difference in smoothness versus changing
left/topdirectly.
Summary & Keywords
- A frame has four stages: style → layout → paint → composite; the property you change decides which stage restarts.
- Reflow (layout) is the most expensive (chained geometry), repaint is medium, composite-only is cheapest.
transform/opacitycan composite only, so they're smooth;width/top/font-sizetrigger reflow, so they're janky.- The compositor thread can handle scrolling and composited animations independently → they may stay smooth even when the main thread is blocked.
- Layout thrashing is the most common trap: reading layout properties forces a synchronous reflow, so always "read first, write later."
- Keywords: rendering pipeline, style, layout/reflow, paint/repaint, composite, layer, layout thrashing, compositor thread,
will-change, FLIP
Further Reading and Next Article Preview
At this point the whole main line inside the browser, from "bytes" to "pixels," is complete: parsing → DOM/CSSOM → event-loop scheduling → rendering pipeline → composited onto the screen.
The whole series:
From here, a natural next direction is navigation: what happens from the moment you type a URL and hit Enter until the page appears — DNS, TCP/TLS, the request, how the response is handed to a renderer process, and the first paint. (Other directions: the compositor and layers in finer detail, or the V8 engine and garbage collection.)
