Reflow & Repaint
reflow & repaint reflow and repaint are main-thread layout and paint invalidations that cause jan reflow (layout) recalculates geometry when
Introduction
Reflow (layout) recalculates geometry when structure or sizes change. Repaint updates pixels when visual appearance changes without layout. Together they dominate main-thread jank when JavaScript mutates DOM/CSS aggressively. Blink marks invalidation roots; Gecko and WebKit use similar dirty-region strategies.
Understanding reflow vs repaint vs composite-only updates separates senior frontend engineers from those who only profile network waterfalls.
Business problem
Business pressure: Dashboard teams polling server state every second update DOM widths — forced reflow loop pegs main thread, INP exceeds 500ms on CrUX, executives see "frozen" UI during board demos.
- Conversion: Checkout spinner that triggers layout each frame feels broken — users double-click pay button.
- Compliance: Dynamic error messages resizing form shift focus — WCAG 3.2.2 violation risk.
- SEO: Less direct, but main-thread congestion delays render for crawlers simulating slow devices.
Why this feature exists
Platform motivation: Engines cannot repaint without knowing geometry (reflow) when layout-affecting properties change. Invalidation system exists to avoid full-page relayout every mutation — but developers can defeat it with read/write thrashing.
- History: "Reflow" terminology from early IE dev docs; modern specs say "layout"; DevTools still shows both terms.
- Alternative rejected: Immutable layout tree per frame — memory and CPU prohibitive.
- Modern role: contain, content-visibility, and ResizeObserver batch layout work.
Browser internals
Inside the engine: Layout invalidation bubbles to containing block chain. Paint invalidation uses damage rects — union of changed areas. Sequential layout forced when JS queries geometry mid-mutation. Blink's LayoutNG caches substructures when inputs unchanged.
- Reflow triggers: width/height, font-size, DOM insert/delete, getComputedStyle on geometric props, window resize.
- Repaint triggers: color, background, visibility, outline — no geometry change.
- Composite skip: transform/opacity on layer — neither reflow nor main-thread repaint.
- DevTools: "Layout" and "Paint" events; "Forced reflow" warning links to triggering JS stack.
JS: element.style.width = '100px' → layout dirtyJS: element.offsetWidth → FORCED SYNC LAYOUTJS: element.style.background = 'red' → paint dirty (if no layout)JS: element.style.transform = '...' → compositor update (if layered)
Rendering workflow
Rendering path: Invalidation coalesces per frame via requestAnimationFrame scheduling in browsers. Multiple DOM writes before rAF typically produce one layout pass. Reads before rAF flush force synchronous layout.
- Critical path: Initial page load layout is full-document; subsequent should be incremental.
- Layout: O(n) subtree worst case; depth and sibling count matter.
- Paint: O(area) — large fixed headers repainting hurt scroll if not composited.
Feature deep dive
Layout thrashing pattern: for each item, write style then read offsetHeight — N forced layouts. Fix: batch writes, read once. ResizeObserver delivers size after layout batch — preferred over polling offsetWidth in loop.
- Fast DOM: DocumentFragment, replaceChildren, classList.toggle batch visual state.
- FLIP technique: First-Last-Invert-Play — measure, mutate, animate with transform (composite).
- contain: strict: Isolates layout subtree — risky if content overflows unknowingly.
- Virtual scrolling: Caps DOM size — reduces reflow scope on data updates.
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Reflow Demo</title><style>#bar { width: 50px; height: 20px; background: #34a853; transition: none; }button { margin: 1rem 0.5rem 1rem 0; }</style></head><body><h1>Layout Thrashing Demo</h1><button id="bad">Bad: read/write loop</button><button id="good">Good: batch writes</button><div id="bar"></div><script>const bar = document.getElementById('bar');document.getElementById('bad').onclick = () => {for (let i = 0; i < 100; i++) {bar.style.width = i + 'px';bar.offsetWidth;}};document.getElementById('good').onclick = () => {bar.style.width = '100px';requestAnimationFrame(() => console.log('width:', bar.offsetWidth));};</script></body></html>
Accessibility analysis
A11y architecture: Reflow from dynamic content changes reading order for zoom users. aria-live polite regions batch announcements — avoid reflowing live region height every 100ms.
- Screen readers: Layout change may move element under cursor — disorienting during announce.
- Keyboard: Focused element reflowed off-screen should scrollIntoView on expand.
- WCAG: Reflow criterion (1.4.10) — content must reflow at 320px without horizontal scroll.
SEO impact
SEO architecture: CLS accumulates from reflow shifts. SEO teams monitor layout instability alongside content visibility — hidden content expansion shifts ranking-adjacent UX signals in CrUX.
- Crawl: Excessive lazy hydration reflow may delay text stability for extraction.
- Rich results: Stable layout helps visual price consistency with schema.
- Core Web Vitals: CLS is direct measure of harmful reflow; INP captures post-reflow handler delay.
Security considerations
Security boundary: Layout timing attacks largely patched; still avoid leaking sensitive state via scroll measurement gadgets in embedded contexts.
- XSS: Malicious script can force expensive reflow loops — DoS on client.
- CSP: Limits script source — reduces injection-driven thrashing.
- Clickjacking: Reflow moves click target under finger — UI redress variant.
Performance impact
Performance: Paul Lewis (Chrome team) "Avoid large, complex layouts and layout thrashing" remains canonical. Target <16ms per frame; layout+painting >10ms leaves little budget for JS.
- LCP: Late reflow when font swaps changes hero size — reserve metrics.
- INP: Handler causing reflow before response paint delays INP event timing.
- CLS: Each unexpected reflow may add to CLS score if elements shift.
Real production example
Production pattern: Google Search results use contain and careful DOM updates to limit reflow scope on infinite scroll. FLIP animations for expanding result cards — transform only, documented in web.dev animation guides.
- Pattern: Read layout in rAF callback; write in previous frame or use CSS classes.
- Monitoring: Long Animation Frames API reports layout duration to RUM.
- Fix: Replaced offsetWidth loop with ResizeObserver — INP p75 dropped 120ms.
Enterprise usage
Enterprise: Spreadsheet-like UIs isolate cell reflow with virtualized rows/columns. Fixed grid tracks prevent cascade reflow across entire sheet.
- Design system: Expand/collapse uses max-height transition sparingly — triggers layout; prefer transform scaleY on composited layer or allow-discrete.
- CMS: Ad slots sized with aspect-ratio CSS — reflow contained to slot.
- CI gates: Performance tests assert no forced reflow in interaction traces.
Common production failures
What breaks in prod: Resize handler calling setState on every pixel of window drag — thousands of reflows per second, browser hang reports.
- Incident: Tooltip positioned with getBoundingClientRect in mousemove — 60 forced layouts/sec.
- SEO regression: Accordion without min-height reserve — CLS 0.38 on FAQ landing pages.
- Perf regression: "Optimize" replaced transform hover with box-shadow transition — full repaints every frame.
Architecture review questions
- Do any hot paths interleave geometric reads and style writes?
- Are window resize handlers debounced and avoiding synchronous layout reads?
- Do expanding UI patterns reserve space or use compositor-safe animation?
- What is layout duration p95 in Long Animation Frames RUM data?
- Are lists virtualized beyond 200 visible rows?
- Does font loading use size-adjust to minimize reflow on swap?
Hands-on project
Project: Implement auto-sizing tooltip two ways: (1) mousemove + getBoundingClientRect each event, (2) cached position updated on rAF. Compare Performance forced reflow count.
- Deliverable: Code + trace screenshots + INP interaction test in DevTools.
- Verify: web.dev layout thrashing article checklist.
- Stretch: Rewrite expand animation with FLIP + transform.
Interview questions
Difference between reflow, repaint, and composite?(Advanced)
Reflow/layout recalculates geometry. Repaint redraws pixels when visuals change without geometry. Composite updates GPU layers for transform/opacity without main-thread layout/paint. Cost generally: reflow ≥ repaint > composite.
Follow-up: Does opacity always composite-only?
What is layout thrashing and how do you fix it?(Intermediate)
Alternating DOM writes and geometric reads force sync layout each iteration. Fix: batch all writes, then read once in requestAnimationFrame; use ResizeObserver; use CSS classes instead of incremental style tweaks in loops.
Follow-up: Name APIs that force layout.
How does contain: layout help?(Advanced)
Tells engine layout changes inside element don't affect outside — limits invalidation subtree. Improves reflow cost for isolated widgets. Must ensure no overflow clipping breaks design.
Follow-up: layout vs strict vs content?
Try it yourself
Edit the HTML, CSS, or JS panels — the preview updates as you type.
Try it yourself
Summary
Reflow and repaint are main-thread layout and paint invalidations that cause jank when misused. Chrome team guidance on web.dev emphasizes avoiding layout thrashing and preferring compositor properties — profile with DevTools Forced reflow warnings.