Research Briefing · Frontend Performance · Aug 8, 2026
Every scroll, every resize, every DOM write can trigger a full CSS layout pass. We benchmarked Chrome to find out what that actually costs — and the numbers change how you should think about your markup.
Layout (reflow) recomputes the geometry of every affected box. The tax rate is set by the number of elements in the layout tree — and the surcharge grows faster than the tree. Our benchmark: one element's width is changed, then a synchronous read forces layout. Median of 15 runs:
| DOM size | Block layout | Flex layout | Grid layout |
|---|---|---|---|
| 100 elements | 0.06 ms | 0.06 ms | 0.10 ms |
| 1,000 elements | 0.42 ms | 0.54 ms | 0.80 ms |
| 10,000 elements | 4.00 ms | 5.22 ms | 7.86 ms |
Three findings worth the price of admission:
1. Layout scales superlinearly at first. 100→1,000 elements (10× the DOM) cost 7× the layout time (0.06→0.42ms). Blink's layout tree does more than visit nodes — it manages formatting contexts, floats, and inline fragmentation, and overhead per node grows with tree depth. 1,000→10,000 is closer to linear (9.5×), but by then you're already paying milliseconds.
2. Grid is the most expensive layout mode at scale. At 10,000 items, grid reflows in 7.86ms — 2.0× block, and 1.5× flex. Grid's track-sizing algorithm resolves min/max contributions per track before assigning positions; with complex templates (minmax, auto-fit, named areas) that resolution is more work per item. Flex is 1.3× block. For the hundreds-of-elements pages most sites are, these ratios are microseconds — the difference only shows up at dashboard/feed/table scale.
3. One reflow can eat half a frame. 7.86ms against a 16.7ms frame budget (60Hz) is 47%. A browser that spends 47% of a frame on layout has already lost: script, paint and compositing still need their share, and any jank that follows is your layout tax coming due. On a mid-range phone (CPU ~4× slower than this desktop) the same tree costs ~30ms — two dropped frames per interaction.
Reproduce it: build a 10,000-item grid with the CSS Grid Generator, then watch DevTools Performance on resize. Or generate the flex version with the Flexbox Generator — the container CSS is copy-ready.
This is the tax nobody sees coming, because the code looks innocent. The browser batches style changes and computes layout lazily — only when a frame is due, or when your code reads something that depends on layout (offsetWidth, getBoundingClientRect(), offsetTop…). Interleave a write with such a read and you force a synchronous flush of every pending layout, every iteration:
for (var i = 0; i < els.length; i++) { els[i].style.width = '41px'; var w = els[i].offsetWidth; }
That's one full reflow per iteration. Our benchmark: 100 elements × 100 operations on a 1,000-element page.
| Pattern | Time | Relative |
|---|---|---|
| Read-write interleaved (thrash) | 61.0 ms | 101× |
| All writes, then one read (batched) | 0.6 ms | 1× |
61ms of blocking work in a single interaction — on a page that's only 1,000 elements. This is the classic cause of "it's fine locally, janky in production": a library or an animation library doing geometry reads inside a loop. The fine is avoidable with one rule: separate reads and writes. Collect all measurements first, apply all mutations second. Modern frameworks (React 18+ auto-batching, Vue's scheduler) give you this for free most of the time; hand-written animation and measurement code is where thrash hides.
Audit tip: in DevTools Performance, look for yellow "Recalculate Style" / "Layout" blocks repeating inside a single task — that's the thrash fingerprint.
The third tax is paid by pages that invalidate layout constantly without realizing it:
Resize listeners doing geometry math. window.resize fires dozens of times per second during a drag-resize; a handler that reads and writes layout per event turns a resize drag into a layout-storm. Debounce it, or better, use ResizeObserver which batches observations into one callback per frame.
Whole-page invalidation. Changing a CSS property on a high-level container (font-size on body, width on a wrapper) invalidates layout for the entire subtree. Prefer targeting the leaf element. If a change is visual only, use transform/opacity — they're compositor-only and never touch layout.
Reading layout in a loop. The same thrash pattern, wearing a different hat: a scroll handler calling getBoundingClientRect() 30× per second on a long list is paying the reflow bill on every scroll tick. Cache what you can; for scroll-position work, IntersectionObserver exists precisely so you don't have to.
The containment escape hatch. content-visibility: auto tells the browser to skip layout and paint for off-screen elements, substituting a placeholder height (contain-intrinsic-size) so the scrollbar stays stable. Chrome's documentation reports initial render of long pages can be up to ~50% faster with it — the off-screen half of your feed never pays the reflow bill until the user scrolls to it. Apply it to below-the-fold sections, long article bodies and feed rows; avoid it on elements you need to search or measure before scroll.
Desktop Chrome computes layout on a CPU that is roughly 4-6× faster than a mid-range phone's. The 7.86ms desktop grid is a ~30ms phone layout — two full frames at 60Hz — before your JavaScript even runs. Every layout tax in this article should be multiplied by the mobile factor when you're deciding whether it matters. If a fix is free (batched reads, containment), apply it regardless; if it costs architectural effort, the mobile threshold is your tiebreaker.
| Symptom | Most likely tax | First move |
|---|---|---|
| Janky animation/scroll on a big page | Reflow bill (Tax 1) | Shrink DOM scale or add content-visibility |
| One interaction hangs for tens of ms | Thrash fine (Tax 2) | Batch reads and writes; find the loop |
| Resize/scroll feels like slideshow | Invisible re-render (Tax 3) | ResizeObserver + IntersectionObserver + transform |
| Layout shows up in every Performance frame | All three | contain: layout on isolated widgets |
content-visibility: auto to below-the-fold sections with contain-intrinsic-size — measure before/after with Lighthouse.Q: Does flexbox or grid hurt SEO? No — layout mode is invisible to search crawlers. But layout performance feeds Core Web Vitals (INP specifically), which are ranking signals. The cost is measured in user experience, then in rankings.
Q: I have a 50-row dashboard. Should I panic? No. 50 rows × 10 cells = 500 elements ≈ 0.2-0.4ms reflow — fine. The tax only compounds past ~1,000 elements in one tree, or when combined with thrash.
Q: Is display: contents a layout hack? It removes an element from the box tree, so its children participate in the parent's layout — useful for semantic wrappers that shouldn't create boxes. It can reduce layout tree depth, but it also breaks some styling/accessibility expectations. Use it deliberately, not as a blanket optimization.
Q: Where do these numbers come from? Chrome 136 headless on a desktop class CPU, generated DOM trees, forced-reflow measurement via synchronous reads, median of 15 runs. Environment specifics (CPU, viewport) shift absolute values but not the ratios — the superlinear scaling, the 2× grid premium and the 101× thrash penalty are structural.
The layout tax has three brackets, and two of them are free to fix. Batching reads and writes kills the 101× thrash fine. content-visibility halves the reflow bill for long pages. What remains — the honest cost of laying out thousands of elements in grid or flex — is a design decision: your dashboard either renders 10,000 boxes, or it pays 8ms per frame. Now you know the price before you spend it.
Related: The Animation Tax · The Image Weight Tax · The Bundle Tax · The Base64 Inflation · Tools: CSS Grid Generator · Flexbox Generator
Benchmarks: jslet layout lab, Chrome 136 headless, Windows 11, forced-reflow via offsetWidth/offsetHeight reads after style mutations, median of 15 (reflow) / 10 (init) / 8 (thrash) runs. DOM generated programmatically (100/1,000/10,000 items; block = plain divs, flex = display:flex; flex-wrap:wrap; gap:1px, grid = display:grid; grid-template-columns:repeat(50, 40px); gap:1px).
content-visibility savings figure from web.dev: content-visibility (up to ~50% initial rendering). Thrift-threshold framing adapted from web.dev: avoid layout thrashing.
All numbers are reproducible in your own browser with the linked generators.
Cite as: jslet Research, 2026 — “The Layout Tax.” jslet.com. All benchmark data is original and reproducible with the linked tools.