Lesson 12 of 30
The browser wants to batch layout work into once per frame. Alternating reads and writes to the DOM defeats that batching entirely, and can turn a cheap loop into a dropped frame.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
Why does reading `element.offsetHeight` right after writing a style force layout to run synchronously?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Normally, the browser is lazy on purpose: when JS changes several styles in a row, it doesn't recompute Layout after each one — it queues the work and runs Layout once, right before the next paint. That batching is what keeps a page fast even when a script touches a dozen elements.
That laziness breaks the moment JS reads a layout-dependent property — offsetHeight, offsetWidth, offsetTop, getBoundingClientRect(), scrollTop, clientWidth, and similar. To answer accurately, the browser can't hand back a stale number from before the pending style changes; it has to run any queued Layout work immediately, synchronously, right there in your script. This is called forced synchronous layout (sometimes "layout thrashing" refers to the anti-pattern that triggers it repeatedly).
Layout thrashing is the specific anti-pattern where code alternates writes and reads inside a loop: write a style, read a layout property, write again, read again. Each read forces a full synchronous Layout recalculation of everything queued so far, so instead of one Layout pass for the whole loop, you get one Layout pass per iteration — turning what should be cheap into a genuine main-thread bottleneck.
// Thrashing: write, then read, on every iteration
function resizeAll(items) {
items.forEach((item) => {
item.style.width = "200px"; // write — queues Layout
const height = item.offsetHeight; // read — forces Layout to run NOW
item.style.height = `${height * 2}px`; // write — queues Layout again
});
}
// Batched: all reads first, then all writes — one Layout pass total
function resizeAll(items) {
const heights = items.map((item) => item.offsetHeight); // read phase
items.forEach((item, i) => {
item.style.width = "200px"; // write phase
item.style.height = `${heights[i] * 2}px`;
});
}
The first version forces one synchronous Layout per item — for a list of 200 items, that's 200 separate Layout recalculations in a single loop. The second version reads every value while nothing is pending (a single, cheap Layout, if any), then writes everything after, so the browser's normal once-per-frame batching applies. Same visible result, dramatically different cost. This is exactly the "read phase, then write phase" pattern behind libraries like FastDOM, and it's the manual discipline those libraries automate.
requestAnimationFrame callbacks) are a common place this hides, because each frame's callback often both measures an element's position and then moves it, and it's easy to interleave those without noticing across iterations of the animation."Why is this loop slow even though each operation looks cheap individually?" is a real diagnostic question, and recognizing an interleaved read/write pattern as forced synchronous layout — then fixing it by separating the read phase from the write phase — is a concrete, demonstrable skill interviewers specifically probe for.