Browser Rendering Pipeline
The browser turns DOM + CSS into pixels through five stages: Style → Layout → Paint → Composite. Knowing which CSS properties trigger which stages — and avoiding layout thrashing — is the core skill behind smooth 60fps UI.
A 60Hz display refreshes every 16.66ms, and the browser has its own overhead, so the practical budget to produce a frame is ~10ms. Miss it and the frame rate drops and content judders — "jank." Responsiveness to discrete interactions is captured by the INP metric (good ≤ 200ms). Source: web.dev, https://web.dev/articles/rendering-performance, 2026-06-12
The five stages
| Stage | What happens |
|---|---|
| JavaScript | Triggers visual change (DOM edits, class toggles, WAAPI). CSS animations/transitions can also drive it. |
| Style | Match selectors to elements, compute final styles. |
| Layout | Compute geometry — size and position. One element's width can reflow the whole subtree. |
| Paint | Fill pixels: text, colors, images, borders, shadows — onto layers (includes rasterization). |
| Composite | Assemble the layers in the correct order onto the screen. |
Three paths through the pipeline
Not every frame touches every stage. Which property you change decides the path:
- Layout property (
width,height,top,left, …) →Style → Layout → Paint → Composite. Most expensive: forces a reflow. - Paint-only property (
background-image,color,box-shadow, …) →Style → Paint → Composite. Skips layout. - Composite-only property (
transform,opacity) →Style → Composite. Cheapest and the only safe path for animation/scroll. Source: web.dev, 2026-06-12
Animate transform and opacity, never top/left/width/height. Chromium also runs page scrolling on the compositor thread, which is why a janky page can still scroll.
Layout thrashing (forced synchronous layout)
Layout thrashing is reading a layout value (offsetWidth, getBoundingClientRect(), scrollTop) after writing to the DOM, in a loop. Each read forces the browser to flush a synchronous layout because the previous write invalidated it, turning one layout into dozens. Tiger Abrodi's self-thread points to his longer article, which gives the same practical fix: batch DOM reads before writes, use off-DOM fragments for bulk changes, and avoid unnecessary reflow triggers. Source: X/@TAbrodi thread, 2026-03-28 (911 likes, 707 bookmarks); Source: Tiger Abrodi blog, 2024-02-27.
// BAD: read → write → read → write — forces layout every iteration
for (const el of items) {
el.style.width = el.offsetWidth + 10 + "px"; // read flushes layout after each write
}
// GOOD: batch all reads, then all writes (the FastDOM pattern)
const widths = items.map((el) => el.offsetWidth); // reads
items.forEach((el, i) => (el.style.width = widths[i] + 10 + "px")); // writes
Caching getBoundingClientRect instead of re-reading it per pointer/zoom event is the same fix applied to a hot interaction loop — exactly what the procedural Canvas renderer in Princeton Tower Defense does to stay smooth.
Practical rules
- Animate only
transform/opacity; hint long-lived animated layers withwill-changesparingly. - Batch DOM reads and writes; never interleave them in a loop.
- Avoid
transition: all— it can transition layout properties you didn't intend. - Reduce paint area and layer count; promote intentionally, not reflexively.
These underpin the interruptible-animation and motion guidance in Interface Micro-Polish and Motion Animation Style, and the scroll-timeline work in Scroll-Driven Animations (CSS) (which runs on the compositor).
Timeline
- 2026-07-04 | Source-reviewed the self-thread and recovered Tiger Abrodi's linked layout-thrashing article; kept this page as the canonical route for the bookmark and repaired the design hub row. Source: X/@TAbrodi thread, 2026-03-28; Source: Tiger Abrodi blog, 2024-02-27
- 2026-06-12 | Page created from the @TAbrodi bookmark (707 bookmarks) explaining the Style → Layout → Paint → Composite pipeline and layout thrashing; promoted out of the X Bookmarks: Design & UI (Feb–Jun 2026) hub into a full concept page. Researched against web.dev rendering-performance (frame budget, three pipeline paths, compositor-only properties). Source: X/@TAbrodi, 2026-03-28 + web.dev, 2026-06-12