CSS performance — layout thrashing and expensive selectors

CSS performance problems are almost never about the stylesheet's file size — they're about how often, and how expensively, the browser has to recompute layout, and a few specific JavaScript/CSS interaction patterns cause that to happen far more than necessary.

Advanced

3 min read

Layout thrashing: reading and writing layout-triggering values in a tight loop

// BAD — forces a synchronous layout recalculation on EVERY iteration
elements.forEach((el) => {
  el.style.width = box.offsetWidth + "px"; // READ offsetWidth, then WRITE width — repeated per element
});

Reading a layout-dependent property (offsetWidth, offsetHeight, getBoundingClientRect(), and several others) forces the browser to make sure layout is fully up to date right now, synchronously, before returning the value — normally the browser can batch and defer layout work, but a read like this cancels that optimization. Interleaving reads and writes of layout properties in a loop — read, write, read, write — forces this forced synchronous layout on every single iteration, since each write invalidates the layout the next read then has to immediately recompute. This is called layout thrashing, and it's a genuinely common, genuinely severe performance bug in code that looks completely innocent.

The fix: batch all reads, then batch all writes

// GOOD — all reads happen first, then all writes — ONE layout recalculation total
const widths = elements.map((el) => box.offsetWidth); // all READS, batched
elements.forEach((el, i) => { el.style.width = widths[i] + "px"; }); // all WRITES, batched

Separating every read from every write means the browser only needs one layout pass for the whole batch of reads, and can defer/batch the writes normally — the same total work, done in a structure the browser can actually optimize, instead of a structure that defeats its optimization on every iteration. This read-then-write batching pattern is the single most impactful fix for layout thrashing, and it requires no new browser API — just reordering the same operations.

Expensive selectors: usually not the bottleneck people assume

/* Widely believed to be slow because of how selectors were historically
   described as matching "right to left" — in practice, modern browser
   selector-matching engines are heavily optimized, and this is rarely
   the actual bottleneck in a real page's performance profile */
.sidebar ul li a { color: blue; }

Selector matching performance is a common target of CSS "best practices" folklore, but in modern browsers it's genuinely rarely the actual bottleneck — browsers match thousands of selectors against a page in a small fraction of a millisecond even for reasonably complex ones. The layout-thrashing pattern above, and the transform/opacity-vs-layout-property distinction from the transitions lesson, are both far more common, far more measurable real-world performance problems than selector complexity — worth spending optimization effort on those first, verified with the browser's own performance profiler, rather than on selector micro-optimization based on outdated assumptions.

Measure before optimizing: the browser's own DevTools tell you what's actually expensive

Chrome DevTools → Performance tab → record a real interaction → look for
"Layout" (purple) and "Recalculate Style" blocks in the flame chart —
these show EXACTLY which operations are costing real time, on the real
page, instead of guessing based on general folklore.

Every claim in this lesson is verifiable directly in the browser's own performance profiler, on the actual page in question — recording a real interaction and looking at the resulting flame chart shows precisely which operations (layout thrashing, expensive paints, genuinely slow selectors, if any) are actually costing time, rather than optimizing based on general rules that may or may not apply to a specific page's actual bottleneck.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What is "layout thrashing"?

2. What's the fix for layout thrashing in a loop that both reads and writes layout properties?

3. According to this lesson, is complex CSS selector matching usually the real performance bottleneck?