almessadi.
Back to Index

Layout Thrashing Starts When DOM Reads and Writes Fight Each Other_

Geometry APIs like `getBoundingClientRect()` are useful, but they become expensive when code repeatedly forces fresh layout in the middle of style and DOM updates.

PublishedDecember 10, 2024
Reading Time6 min read

The browser wants to batch style calculation and layout work. Performance problems show up when application code keeps interrupting that plan by alternating DOM writes and layout reads in tight loops.

That pattern is usually called layout thrashing.

The Bug Pattern

This is the expensive shape:

for (const item of items) {
  item.style.width = "200px";
  const rect = item.getBoundingClientRect();
  item.style.height = `${rect.width}px`;
}

Every call to getBoundingClientRect() may force the browser to flush pending layout work before it can answer. If this runs during scroll, resize, or animation, the UI gets janky fast.

Better Pattern

Batch reads first. Batch writes second:

const widths = items.map((item) => item.getBoundingClientRect().width);

items.forEach((item, index) => {
  item.style.width = "200px";
  item.style.height = `${widths[index]}px`;
});

That is not the only possible fix, but it shows the underlying rule: avoid forcing the browser to repeatedly recompute layout inside frame-sensitive code.

When This Actually Matters

You usually feel it in:

  • scroll handlers
  • animation loops
  • drag-and-drop interactions
  • large lists with direct DOM manipulation

The browser is fast when you let it batch work. It becomes expensive when you make it answer geometry questions immediately after every write.

If the code has to run during animation or interaction, requestAnimationFrame is often the right place to schedule batched DOM work so reads and writes happen in a more predictable frame boundary.

Further Reading