What's happening when a website loads?
When you visit a website, your browser reads the code, it creates the DOM, it reads the CSS, it builds the CSSOM, figures out where everything should go, and then draws it on your screen. This process includes two main concepts: repaint and reflow.
What is a repaint?
A repaint happens when you change the appearance of an element, but NOT the sizes or positions. Think it as changing a text or background color, opacity, borders or box shadows.
element.style.color = "blue";
element.style.backgroundColor = "yellow";
When this happens, the browser just needs to redraw the pixels on screen. In general, this is somewhat fast.
What is a reflow?
A reflow (or "layout") happens when you change the size or position of an element. Think about it as a result of changing width, height, padding or margins, adding or removing elements from the page, showing/hiding elements with display property, or changing the font size.
newElement.style.width = "500px";
newElement.style.padding = "20px";
document.body.appendChild(newElement);
When this happens, the browser has to:
- Recalculate where everything should be
- Figure out the new size of elements
- Update all the affected elements
- THEN repaint everything
That's why reflows are a bit expensive.
Why Should We Care?
Both repaints and reflows make the browser do work, which can make your website feel slow, especially when you trigger them often.
Imagine you're building a feature that updates a user's score:
for (let i = 0; i < 100; i++) {
element.style.width = (100 + i) + "px"; // Reflow!
element.textContent = i; // Reflow!
}
Every loop iteration triggers a reflow because we're changing the width and content. That's 100+ potential reflows! Modern browsers do batch some of these automatically, but you still want to minimize layout recalculations.
element.textContent = 99;
element.style.width = "199px";
Prefer to do all the changes at once. The browser will likely batch these into a single reflow cycle instead of triggering multiple sequential ones.
Tips to Avoid Performance Problems
- Batch your changes
// Good
element.style.cssText = "width: 100px; height: 100px; color: red;";
// Not as good
element.style.width = "100px";
element.style.height = "100px";
element.style.color = "red";
- Avoid reading layout properties in loops
// Bad — alternating reads and writes force recalculation
for (let i = 0; i < elements.length; i++) {
const currentWidth = elements[i].offsetWidth; // READ — forces layout recalculation
elements[i].style.width = (currentWidth + 10) + "px"; // WRITE — invalidates layout
}
// Good — batch all reads first, then writes
const widths = Array.from(elements).map(el => el.offsetWidth);
widths.forEach((width, i) => {
elements[i].style.width = (width + 10) + "px";
});
- Use classes instead of inline styles (similar with 1)
// Good — batches all CSS changes together and is more maintainable
element.classList.add("highlighted");
// Not as good — multiple individual property assignments can trigger separate reflows
element.style.color = "blue";
element.style.backgroundColor = "yellow";
element.style.fontWeight = "bold";
CSS classes batch multiple properties into a single reflow cycle, and keep your styling logic separate from your JavaScript.
- Use transforms for animations
// Good for animations
element.style.transform = "translateX(100px)";
// Causes reflow
element.style.left = "100px";
Transforms are cheaper because they skip the Layout and Paint phases of the rendering pipeline and only trigger Composite (which happens on the GPU). Position changes like left, top, width, and height require the browser to recalculate the entire layout.
- Use requestAnimationFrame for Coordinated Updates
When you need to perform multiple DOM reads and writes in animations, use requestAnimationFrame to ensure they're batched together in the browser's rendering cycle:
// Good — batches all changes into one frame
requestAnimationFrame(() => {
elements.forEach(el => {
el.style.transform = "translateX(100px)";
});
});
// Less efficient — each update happens immediately, may cause multiple recalculations
elements.forEach(el => {
el.style.transform = "translateX(100px)";
});
Modern Browser Considerations
Browsers have become smarter about batching DOM operations. Modern engines like V8 (Chrome) and SpiderMonkey (Firefox) automatically defer and batch layout recalculations to some degree. However, this doesn't mean poor practices are harmless, you can still force synchronous reflows by reading layout properties like offsetWidth, clientHeight, or getBoundingClientRect() immediately after writing styles. Avoiding these patterns ensures your code stays performant across all browsers and circumstances.
The Bottom Line
☑️ Repaint = Change colors/appearance → relatively fast
☑️ Reflow = Change size/position/layout → slower, more expensive
☑️ Both slow down your site if you do too many
☑️ Group changes together when possible
☑️ Use CSS transforms for animations
Next time a website feels slow, there's a good chance it's because the browser is constantly doing reflows and repaints.
Further Reading
- MDN: Visual formatting model - Background on how browsers lay out pages
- web.dev: Avoid large, complex layouts and layout thrashing - Google's deep dive specifically on layout thrashing and how to avoid it
- web.dev: Rendering Performance - The full pixel pipeline (JS → Style → Layout → Paint → Composite)
- CSS Triggers - A reference showing exactly which CSS properties trigger layout, paint, or composite
- Chrome DevTools: Analyze runtime performance - How to actually profile and see reflows/repaints happening in your own site


Top comments (0)