Learning on Web Dev Open is free for all.

Take It Apart > The DOM as an APIReading and writing cost differently
Phase 01The DOM as an API70 of 434

Reading and writing cost differently

Writing to the DOM is queued and cheap. Reading certain properties forces the browser to do all the queued work immediately, and doing that in a loop is a well-known way to make a fast page slow.

Concept16 minAI pair

The browser is lazy on purpose. When you change a style, it does not immediately recompute the layout, it marks the page dirty and batches the work until it needs the result, usually just before the next frame. That laziness is most of why DOM manipulation is fast enough to be usable at all.

But some things you can read are only knowable after layout has run. offsetTop, offsetHeight, getBoundingClientRect(), scrollTop, getComputedStyle(), focus(), ask for any of these and the browser cannot answer from stale data. It must flush everything pending and lay out immediately. That is a forced synchronous layout, and one of them is fine.

The problem is the loop. Write a style, read a geometry, write, read, fifty times. Every read forces a layout that the write just invalidated, so you have caused fifty layouts where one would have done. This is layout thrashing, and it is the most common performance bug in hand-written DOM code, common enough that it has a name and that Chrome DevTools flags it specifically.

The fix is structural and simple: read everything first, then write everything. The reads all come from one layout because nothing invalidated it in between; the writes all batch because nothing read in between. Fifty layouts become one, and the code is usually clearer as well.

The instrument below runs both versions side by side with a forced-layout counter. Watching it climb to fifty in one column and stay at one in the other is more persuasive than the paragraph you just read. Then do the reorder exercise at the bottom, three statements, and getting them in the right order is the entire skill.

Fifty forced layouts, or one

Interleavedwrite, read, write, read
for (const box of boxes) {           // 50 of them
  box.style.width = grow(box) + 'px'; // write: marks layout dirty
  total += box.offsetHeight;          // read: forces layout NOW
}

forced layouts

0

iterations

0

frame

0.0ms

One frame: 16.7ms to spend

16.7

Not run yet.

Batchedread all, then write all
const heights = boxes.map((b) => b.offsetHeight); // read all
const next = heights.map(grow);                   // no DOM
boxes.forEach((b, i) => {                         // write all
  b.style.width = next[i] + 'px';
});

forced layouts

0

iterations

0

frame

0.0ms

One frame: 16.7ms to spend

16.7

Not run yet.

Reads that force layout: click one

All of these are questions the browser cannot answer from a dirty layout. None of them is slow on its own: each one is slow when a write happened on the line above.

Rewrite it: put the three statements in order
// what it is now: 50 forced layouts
for (const b of boxes) {
  b.style.width = b.offsetWidth * 1.1 + 'px';
}
  1. 1
    boxes.forEach((b, i) => { b.style.width = `${next[i]}px`; });write pass
  2. 2
    const widths = boxes.map((b) => b.offsetWidth);read pass
  3. 3
    const next = widths.map((w) => w * 1.1);arithmetic, touches no DOM

Every DOM read first, then the arithmetic, then every DOM write. Reorder and check.

Run each version. Watch the forced-layout counter rather than the milliseconds: the counter is the part that does not depend on your hardware.

Run both. The counter is the lesson; the milliseconds are a model and the component says so.

To find it in real code: open the Performance panel, record an interaction, and look for the purple layout blocks. A long row of them is thrashing, and Chrome will often label the offending read for you. Then look at where it is: in your own code, or in a library, or, increasingly often, in something a model generated, because the interleaved version is the one that reads more naturally and therefore appears more in training data.

Worth reading

You should now be able to

  • Explain forced synchronous layout
  • Identify the reads that trigger it
  • Restructure a loop to batch reads and writes
  • Find the pattern in the Performance panel
Ask the community

Loading…