Layout Reflow vs Repaint: Forced Synchronous Layouts & Geometry Reads
- Last updated
- Prerequisites:
- CSS layout and the box model
- Understanding of the rendering pipeline
- browser
- reflow
- repaint
- layout
- forced-layout
- performance
- geometry
Read at your depth
The practical view
Reflow (layout) recomputes the geometry (position/size) of elements; repaint redraws the pixels. Reading layout properties like offsetWidth, getBoundingClientRect(), offsetTop forces a synchronous reflow if the layout is dirty — the browser cannot answer without computing layout first. Writing styles (changing width/height) marks layout dirty but batches the actual reflow until needed. The classic perf bug is a read-write-read-write loop (layout thrashing): each read forces a reflow, making the loop O(N) reflows. Fix: batch reads together and writes together, or use modern APIs (IntersectionObserver, ResizeObserver).
The same idea in other frameworks
React re-renders can trigger FSL when effects measure the DOM; useLayoutEffect reads pre-paint by design.Angular change detection + DOM reads in lifecycle hooks can force layout; CDK uses ResizeObserver to avoid it.Vue watchers reading layout after DOM updates force reflow; batching reads avoids thrash.Svelte $effect timing around DOM writes/reads determines whether reads force synchronous layout.Legacy vs modern
Layout-thrashing read/write loop vs batched measurements
Interleaving geometry reads and style writes forces a synchronous reflow per iteration; batching reads and writes collapses the work into one or two flushes.
for (const el of els) {
const w = el.offsetWidth; // ❌ OLD: forces layout (dirty from previous write)
el.style.width = (w + 10) + 'px'; // write dirties layout again
}const widths = [...els].map(el => el.offsetWidth); // ✅ NEW: all reads together
els.forEach((el, i) => { el.style.width = (widths[i] + 10) + 'px'; }); // ✅ NEW: all writesInterview gotchas
Context
This is the core performance question about layout — interviewers want the dirty-flag mechanism and the concrete list of properties that force reflow, not just 'reads are slow'.
The mechanical answer
A forced synchronous layout (FSL) happens when you read a geometry-dependent property while the layout is dirty: the browser cannot return a current value without first running the layout pass, so it flushes synchronously, regardless of any batching. Triggering properties include offsetTop/Left/Width/Height, clientTop/Left/Width/Height, getBoundingClientRect(), getComputedStyle() (for some properties), scrollTop/Left/Width/Height, and range/selection geometry. In a loop that alternates writes and these reads, every iteration forces a reflow — the worst pattern. Avoiding FSL means separating reads from writes (batch them), caching measurements, and using APIs that report without forcing layout synchronously (IntersectionObserver, ResizeObserver, or rAF-timed measurement).
Trap
The naive answer is 'layout happens once when the page loads'. Layout is lazy and flushes on reads or before paint. Another trap: thinking getComputedStyle is always safe — reading layout-dependent properties from it also forces layout. Candidates who list the offending properties and the dirty-flag flush mechanism show real performance understanding.
Context
This tests the pipeline phases — interviewers want the geometry-vs-pixels distinction and the cascade of work each change triggers, which determines optimization strategy.
The mechanical answer
Reflow (layout) recomputes the geometry of elements: position, size, and the flow of descendants and (potentially) ancestors — a change to width or position can cascade through the layout tree. Repaint redraws pixels without recomputing geometry: changing color, background, or visibility only invalidates the paint of that element's layer. Reflow almost always triggers a repaint afterward, so it is strictly more expensive. A pure color change skips layout entirely (if no geometry is affected) and only repaints. Transforms and opacity are even cheaper: they are composited on the GPU without layout or repaint (compositor-only). The optimization hierarchy: compositor-only (transform/opacity) < repaint (color) < reflow (geometry).
Trap
The naive answer is 'repaint is always cheap'. Repainting a large layer is not free, and layout-triggered repaints compound the cost. Another trap: assuming transform causes reflow — it does not (it's composited); animating transform/opacity is the standard way to avoid layout/paint during animations. Candidates who place each property change on the layout→paint→composite scale show real depth.