Crux Read real DOM-manipulation snippets, predict which pipeline stages each one re-runs, and pick the highest-leverage fix before reaching for the compositor.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min
Render bugs are diagnosed in the handler code and the DevTools strip, not in the abstract. Read each snippet, predict which stages re-run, and choose the fix a senior engineer makes first.
Goal
Practise the loop you run in every jank incident: read the hot path, predict where the layout flush happens, and reach for the structural fix — batch reads, use transform — before touching anything else.
Snippet 1 — the resize loop
function applyRowWidths(rows, padding) { for (const row of rows) { const w = row.offsetWidth; // read: forces a layout flush row.style.width = (w + padding) + 'px'; // write: marks layout dirty }}
Quiz
Completed
With 5,000 rows this blocks the main thread for seconds. What is happening, and what is the single highest-leverage fix?
Heads-up Allocation is not the bottleneck here. The cost is N synchronous layout flushes from interleaving reads and writes; batching reads before writes drops it to one layout.
Heads-up rAF batches the invocation, not the invalidation. A read after a write inside the callback still forces a synchronous flush. You must separate reads from writes.
Heads-up scaleX would avoid layout on the write, but the offsetWidth read still forces a flush each iteration. The structural problem is the interleaved read/write, not the property.
Snippet 2 — two animation candidates
/* A */ .card.move-a { top: 200px; transition: top 300ms; }/* B */ .card.move-b { transform: translateY(200px); transition: transform 300ms; }
Quiz
Completed
Both animate a card down by 200px over 300ms. Which stages re-run per frame for A vs B, and which do you ship?
Heads-up transition only describes interpolation. top is still flow-affecting, so A re-runs layout each frame; only transform/opacity stay on the compositor.
Heads-up Property simplicity is irrelevant. top invalidates layout on the main thread; transform on a promoted layer skips layout and paint entirely.
Heads-up transform moves an already-painted bitmap on the compositor; the content is unchanged, so no repaint. That is exactly why B is cheap.
Snippet 3 — interleaved across two helpers
function expand(items) { items.forEach((el) => { const top = el.offsetTop; // read el.style.height = top / 2 + 'px'; // write const h = el.getBoundingClientRect().height; // read again — flush! el.style.marginTop = h / 4 + 'px'; // write again });}
Quiz
Completed
DevTools logs a Forced reflow while executing JavaScript warning pointing at this function. How many forced layouts per item, and what is the fix?
Heads-up Reads do not coalesce across a write. The write between the two reads re-dirties layout, so the second read forces a second flush — two per item.
Heads-up getBoundingClientRect returns live geometry and forces a flush of pending writes. It is one of the canonical layout-triggering reads.
Heads-up The loop construct is irrelevant. The reflow comes from reading geometry after writing styles; the fix is separating reads from writes, not the iteration style.
Snippet 4 — reacting to a resize
const box = document.querySelector('.panel');box.addEventListener('transitionend', () => { requestAnimationFrame(() => { const w = box.offsetWidth; // inside rAF, before style/layout step sibling.style.width = w + 'px'; });});
Quiz
Completed
The developer used rAF expecting the offsetWidth read to be free. What actually happens, and which API is the right tool to read a new size without a forced flush?
Heads-up rAF runs before style/layout in the frame; a geometry read there still forces a flush. The read being inside rAF does not make it free.
Heads-up IntersectionObserver fires asynchronously across frames and reports intersection, not element size. It is not the tool for a same-frame size read; ResizeObserver is.
Heads-up MutationObserver fires on DOM mutations, not after layout. Reading geometry in its callback can itself force a flush. ResizeObserver is the post-layout hook.
Recap
Every render incident is read in code: interleaved geometry reads and style writes force one layout flush per read, so N rows cost N layouts — batch all reads before all writes. top animates on the main thread, transform on the compositor, so prefer transform for movement. rAF runs before the style/layout step, so reads there still flush; ResizeObserver is the post-layout, pre-paint hook for size-reactive updates. Predict the stage from the property and the read order, fix structurally, then confirm in the DevTools strip.