Reflow, Repaint and Batching

A reflow (layout) recomputes box sizes and positions; a repaint redraws pixels without moving anything (How CSS Becomes Pixels maps properties to stages). Writes such as el.style.width = '50px' do not reflow on the spot: the engine marks boxes dirty and does the work once, before the next frame.

Reads break that deal. Asking for offsetWidth, clientHeight, scrollTop, getBoundingClientRect() or a computed width while layout is dirty makes the browser run layout synchronously, inside your script, to return a correct number. One forced layout per loop iteration is layout thrashing, measured below in headless Chrome 152 1 .

Interleaved versus batched reads and writes on 2,000 elementsJavaScript
const bars = Array.from({ length: 2000 }, () => {
  const bar = document.body.appendChild(document.createElement('div'));
  bar.style.cssText = 'height: 2px; width: 100px; background: teal';
  return bar;
});
function thrash() {   // read, write, read, write: every read forces a layout
  for (const bar of bars) bar.style.width = `${bar.offsetWidth + 1}px`;
}
function batched() {  // all reads first, then all writes
  const widths = bars.map((bar) => bar.offsetWidth);
  bars.forEach((bar, i) => { bar.style.width = `${widths[i] + 1}px`; });
}
for (const fn of [thrash, batched]) {
  const t0 = performance.now();
  fn();
  bars[0].offsetWidth;  // include the final layout in the timing
  console.log(`${fn.name}: ${(performance.now() - t0).toFixed(1)} ms`);
}
Output
thrash: 1140.5 ms
batched: 6.1 ms

The same writes cost almost 190 times more when interleaved with reads. Swapping offsetWidth for getComputedStyle(bar).color in the thrashing loop took 8.7 ms in the same browser: color needs style, not layout.

To keep DOM code fast: