Performance and DevTools

Avoid layout thrashing

Batch DOM reads and writes so JavaScript does not repeatedly force synchronous layout inside a loop.

The browser is lazy about layout, and that’s a good thing. When you change a style, it doesn’t recompute positions right away. It marks the layout as dirty and waits until it needs the result, usually right before the next paint. Ten style changes in a row cost one layout.

You break this laziness the moment you ask for geometry. Reading offsetHeight, getBoundingClientRect(), or scrollTop after a write forces the browser to compute layout right now, synchronously, so it can give you a correct answer. We call this a forced synchronous layout.

One forced layout is fine. The problem is doing it in a loop:

for (const item of items) {
  item.style.width = `${containerWidth}px`
  console.log(item.offsetHeight)
}

Look at the order. Write, read, write, read. Each write dirties the layout. Each read forces it to be recomputed. With 500 items, the browser lays out the page 500 times inside one event handler. That’s layout thrashing.

Separate reads from writes

The fix is to change the order, not the work. Do all the reads first, then all the writes:

const heights = items.map(item => item.offsetHeight)

items.forEach((item, index) => {
  item.style.width = `${containerWidth}px`
  item.dataset.previousHeight = heights[index]
})

The first line forces layout once, on the first offsetHeight, and the rest read from a clean layout. Then the loop only writes, and the browser recomputes layout once, before the next paint. Two layouts instead of 500.

Your real code will compute something different, but the rule holds: group the reads, compute, group the writes. And if you need the same dimension several times, read it once and keep it in a variable instead of asking the browser again.

Don’t over-correct

Don’t go through your codebase deleting every geometry read. Most of them are harmless. A read in a click handler that runs once costs nothing measurable.

Find the real ones with a recording. Record the slow interaction in the Performance panel and look at the Main track for many small Layout blocks in a row, tied to the same function. DevTools marks them with a warning in the Summary tab that says the layout was forced. Fix that loop. One measured fix beats wrapping random DOM code in scheduling helpers.

To see it for yourself, create 500 <div> elements on a blank page and run both versions while recording. Compare the number of Layout events and the time spent in them. Run it a few times, because on such a tiny example profiling noise can be bigger than the effect you’re measuring.

Lesson completed