gotchajavascriptMajor
Layout thrashing: batching DOM reads and writes to prevent forced synchronous layout
Viewed 0 times
layout thrashingforced synchronous layoutreflowgetBoundingClientRectoffsetWidthFastDOMbatch DOM
Problem
JavaScript code that alternates DOM reads (getBoundingClientRect, offsetTop) with DOM writes (style changes) inside a loop causes the browser to perform a full layout recalculation on every iteration, multiplying what could be a single layout into dozens.
Solution
Batch all DOM reads together, then all DOM writes together. Never alternate read-write-read-write.
// Bad: alternating read/write causes layout thrashing
for (const el of elements) {
const width = el.offsetWidth; // READ — forces layout
el.style.width = width * 1.5 + 'px'; // WRITE — invalidates layout
// Next iteration: READ forces layout again
}
// Good: all reads first, all writes after
const widths = elements.map(el => el.offsetWidth); // all READs
elements.forEach((el, i) => {
el.style.width = widths[i] * 1.5 + 'px'; // all WRITEs
});
// Alternative: FastDOM library schedules reads/writes in rAF
import fastdom from 'fastdom';
fastdom.measure(() => {
const width = el.offsetWidth;
fastdom.mutate(() => { el.style.width = width * 1.5 + 'px'; });
});
// Bad: alternating read/write causes layout thrashing
for (const el of elements) {
const width = el.offsetWidth; // READ — forces layout
el.style.width = width * 1.5 + 'px'; // WRITE — invalidates layout
// Next iteration: READ forces layout again
}
// Good: all reads first, all writes after
const widths = elements.map(el => el.offsetWidth); // all READs
elements.forEach((el, i) => {
el.style.width = widths[i] * 1.5 + 'px'; // all WRITEs
});
// Alternative: FastDOM library schedules reads/writes in rAF
import fastdom from 'fastdom';
fastdom.measure(() => {
const width = el.offsetWidth;
fastdom.mutate(() => { el.style.width = width * 1.5 + 'px'; });
});
Why
Layout (reflow) is expensive. Writing to the DOM invalidates the layout cache. If JS then reads a layout property, the browser must immediately recalculate layout to return the correct value, even mid-frame. Batching ensures layout is only invalidated once per frame.
Gotchas
- Properties that trigger layout (forced synchronous layout): offsetTop, offsetLeft, offsetWidth, offsetHeight, getBoundingClientRect, scrollTop, clientWidth, getComputedStyle
- Use CSS transform instead of top/left for animations — transforms do not trigger layout, only compositing
- will-change: transform elevates an element to its own compositor layer, isolating it from layout changes
- Chrome DevTools: purple 'Layout' bars in the flame chart indicate layout recalculations
Code Snippets
CSS transform avoids layout and paint
// Use CSS transform for animations to skip layout and paint
// Only compositing is needed — runs on compositor thread
element.style.transform = 'translateX(100px)'; // GOOD
element.style.left = '100px'; // BAD — triggers layoutContext
When reading and writing DOM properties inside loops or animation callbacks
Revisions (0)
No revisions yet.