Suggest an editImprove this articleRefine the answer for “When reflow and repaint occur in browser”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Reflow** happens when the browser recalculates element geometry after a layout-affecting change. **Repaint** redraws pixels for visual changes that do not shift anything. ```js box.style.background = 'blue'; // repaint only box.style.width = '200px'; // reflow + repaint ``` **Key:** every reflow triggers a repaint. Use `transform` and `opacity` for animations to skip both.Shown above the full answer for quick recall.Answer (EN)Image**Reflow** forces the browser to recalculate element geometry when layout-affecting properties change. **Repaint** redraws pixels for visual changes that do not shift any element. ## Theory ### TL;DR - Reflow is like rearranging furniture: when one piece moves, everything around it can shift too - Repaint is like repainting a wall: nothing moves, only pixel colors change - Every reflow triggers a repaint, but a repaint does not trigger a reflow - Width or height change = reflow + repaint. Color change = repaint only - `transform` and `opacity` animations can skip both entirely (compositor-only) ### Quick Example ```html <div id="box" style="width:100px; height:100px; background:red; margin:20px;"></div> <div id="sibling">Sibling element</div> <script> const box = document.getElementById('box'); // Repaint only: color does not affect geometry, sibling stays in place box.style.background = 'blue'; // Reflow + repaint: height change shifts the sibling element down box.style.height = '200px'; </script> ``` Color change is fast because the browser only redraws pixels. Height change forces a full layout recalculation: the browser walks up to the parent, then down to the sibling, updating coordinates for everything in scope. ### How the Browser Processes Changes Browsers build a render tree from the DOM and CSSOM. During layout (reflow), the engine assigns exact pixel coordinates and sizes to every node using block formatting contexts. That result gets rasterized to screen via GPU in Blink/Chromium, or CPU in Gecko. When you change something, the browser marks affected nodes as "dirty". On the next paint frame (roughly 16ms at 60fps), it recalculates the layout subtree starting from the changed node, propagates up to ancestors and down to descendants and siblings. That propagation is what makes reflow expensive on complex pages. Repaint skips the geometry pass entirely. The engine re-rasterizes only the pixels for the visual property that changed. No neighbor elements are touched. ### Key Difference Reflow rebuilds element geometry from the changed node outward: parents, children, and siblings can all shift. Repaint only updates the pixels of the element itself. This is why `display: none` causes a reflow (the element leaves document flow and its space collapses) while `visibility: hidden` causes only a repaint (space stays reserved, nothing shifts). ### When Reflow Occurs - Adding or removing DOM elements - Changing `width`, `height`, `margin`, `padding`, `border-width` - Changing `position`, `top`, `left`, `float` - Changing `font-size`, `font-family`, `line-height` - Switching `display` from `none` to `block` or back - Window resize - Reading layout properties: `offsetWidth`, `offsetHeight`, `getBoundingClientRect()` That last point trips many developers. Reading a layout property mid-frame forces the browser to flush its dirty-node queue and recalculate layout immediately, even during an animation. ### When Repaint Occurs (Without Reflow) - Changing `color`, `background-color`, `background-image` - Changing `border-color`, `box-shadow`, `outline` - Switching `visibility` (not `display`) - Changing `opacity` on non-composited elements ### Forced Synchronous Reflow ```html <div id="anim" style="width:100px; transition:width 1s; background:blue;"></div> <script> const el = document.getElementById('anim'); el.style.width = '200px'; // queues a dirty node, reflow not yet done // Reading offsetWidth here forces the browser to flush immediately // This destroys animation smoothness console.log(el.offsetWidth); // forced synchronous reflow // Fix: batch all reads before any writes in the same frame </script> ``` This pattern shows up in Chrome DevTools as yellow "Layout" spikes in the performance flame chart. React's `useLayoutEffect` intentionally runs after reflow and before repaint, which is exactly why it blocks the paint. ### Common Mistakes **Read-modify in a loop** ```js // Bad: each offsetTop read flushes the dirty queue - N reflows for N elements for (let i = 0; i < elements.length; i++) { elements[i].style.top = elements[i].offsetTop + 10 + 'px'; } // Fix: batch reads first, then writes const tops = elements.map(el => el.offsetTop); // one reflow elements.forEach((el, i) => { el.style.top = tops[i] + 10 + 'px'; // writes only }); ``` **Animating layout properties** ```js // Bad: triggers reflow on every animation frame element.style.left = currentX + 'px'; // Good: compositor handles this, zero reflow element.style.transform = `translateX(${currentX}px)`; ``` **Table layouts with dynamic rows** ```css /* Adding one row reflows all cells in the table grid */ table { display: table; } /* Fix: use flexbox or grid for dynamic layouts */ .list { display: flex; flex-direction: column; } ``` **Font loading without font-display** ```css /* Without font-display, text reflows when the font finally loads */ @font-face { src: url(font.woff2); font-display: swap; /* prevents layout shift on font load */ } ``` ### Real-world Usage - **React**: `useLayoutEffect` runs post-reflow, pre-repaint for DOM measurements, used in react-window for virtual list calculations - **GSAP**: `xPercent` and `scaleX` avoid reflow vs animating `left` or `width` - **Swiper.js**: `contain: layout` on slide containers isolates carousel reflow from the rest of the page - **Chart.js**: renders to `<canvas>`, which skips DOM reflow entirely for chart animations I once debugged a carousel where reading `offsetWidth` inside the animation loop caused 60 reflows per second. Moving the read outside the loop dropped layout time from ~60ms to under 2ms per frame. ### Follow-up Questions **Q:** What is the difference between reflow, repaint, and compositing? **A:** Compositing blends already-painted layers on the GPU without touching the DOM. `transform` and `opacity` on promoted layers only composite. Reflow handles geometry, repaint handles pixels, compositing just blends layers. **Q:** How do you measure reflows in the browser? **A:** Open Chrome DevTools, go to the Performance panel, record an interaction, then look for "Layout" events over 1ms. "Recalculate Style" is CSSOM work, not a reflow. **Q:** Does `position: fixed` remove an element from reflow? **A:** No. Fixed elements still cause reflow. `contain: layout` is what actually isolates reflow propagation on a component. **Q:** How does CSS `contain` change reflow propagation? **A:** `contain: layout` tells the browser that nothing inside the element affects layout outside it. This stops reflow from walking up the tree, which matters for web components and large component libraries. **Q:** Why does `display: none` cause reflow but `visibility: hidden` does not? **A:** `display: none` removes the element from document flow entirely. Siblings shift, parents resize. `visibility: hidden` keeps the space, so nothing around it moves. **Q:** In React StrictMode, why do double reflows happen? **A:** StrictMode intentionally invokes effects twice in development to surface side effects. In production, each lifecycle runs once. ## Examples ### Color Change vs. Size Change ```html <!DOCTYPE html> <html> <body> <div id="box" style="width:100px; height:100px; background:red; margin:20px;">Box</div> <div id="sibling" style="background:lightgray; padding:10px;">Sibling</div> <script> const box = document.getElementById('box'); // Repaint only: sibling stays exactly where it is box.style.background = 'blue'; setTimeout(() => { // Reflow: sibling shifts down as box grows taller box.style.height = '200px'; }, 1000); </script> </body> </html> ``` Color swap goes through repaint only. Height change recalculates positions for both the `box` and the sibling. At 100 elements changing 60 times per second, the difference in frame time is not subtle. ### Batching Reads and Writes in a Dashboard Grid ```js // Context: resizing multiple cards in a dashboard grid // Bad: interleaved reads and writes, one reflow per card function resizeCardsBad(cards) { cards.forEach(card => { const h = card.offsetHeight; // read forces reflow card.style.height = h + 50 + 'px'; // write }); } // Good: batch all reads first, then all writes (one reflow total) function resizeCardsGood(cards) { const heights = cards.map(card => card.offsetHeight); // all reads cards.forEach((card, i) => { card.style.height = heights[i] + 50 + 'px'; // all writes }); } // Alternative: ResizeObserver removes explicit layout reads from your code const observer = new ResizeObserver(entries => { entries.forEach(entry => { const { height } = entry.contentRect; // browser-provided, no forced reflow entry.target.setAttribute('data-height', height); }); }); cards.forEach(card => observer.observe(card)); ``` The batched version reduces N reflows to 1. `ResizeObserver` removes explicit layout reads entirely and lets the browser schedule measurement work on its own. ### Compositor-only Animation (Zero Reflow, Zero Repaint) ```js // Bad: left triggers a full reflow on every animation frame function animateBad(element) { let pos = 0; function step() { pos += 2; element.style.left = pos + 'px'; // reflow every frame if (pos < 300) requestAnimationFrame(step); } requestAnimationFrame(step); } // Good: transform runs on the compositor thread, zero reflow, zero repaint function animateGood(element) { let pos = 0; element.style.willChange = 'transform'; // promote to its own GPU layer function step() { pos += 2; element.style.transform = `translateX(${pos}px)`; // compositor-only if (pos < 300) requestAnimationFrame(step); } requestAnimationFrame(step); } ``` At 60fps, the bad version runs 60 reflows per second and can block the main thread. The good version runs zero. This is usually the single biggest animation performance improvement in frontend codebases.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.