You have probably used a website that felt sluggish. You click a button and the page freezes for a moment. You scroll and everything jerks. The animations look choppy. Nothing is broken exactly — it just feels bad.

In most cases the cause is the same: the JavaScript is making the browser do too much layout work at the wrong time. The code is correct but unoptimised, and the browser is constantly being asked to recalculate the position and size of elements on screen.

This guide explains what is actually happening inside the browser when this occurs, why some DOM operations are much more expensive than others, and the practical techniques you can use to fix it — all in plain, beginner-friendly English.

How the Browser Renders a Page

Before we talk about problems, you need a rough picture of how the browser turns your HTML and CSS into pixels on screen. It goes through these steps every time something changes:

  • Parse — the browser reads your HTML and builds the DOM tree (a list of all your elements and how they nest)
  • Style — it figures out which CSS rules apply to each element
  • Layout — it calculates the exact size and position of every element on the page
  • Paint — it fills in the pixels, drawing backgrounds, text, borders and images
  • Composite — it combines all the layers and sends the final image to the screen

The important thing to understand is this: Layout and Paint are expensive steps. They take real time. When your JavaScript forces the browser to repeat these steps over and over, the page feels slow and janky.

ℹ️ The 60fps target: a smooth web page runs at 60 frames per second. That means the browser has roughly 16 milliseconds to do all its work between each frame. If your JavaScript takes longer than that, the frame gets dropped and the user sees a stutter.

What Is a Reflow

A reflow (also called layout) happens when the browser has to recalculate the size and position of elements on the page. It is the expensive step.

Think of it like rearranging furniture in a room. If you move the sofa, you might also have to move the coffee table to make room, and then shift the rug, and then the lamp no longer fits where it was. One change causes a chain of recalculations. That is what a reflow is.

The worst part about reflows is that they are synchronous by default. When you trigger a reflow, the browser stops everything it was doing, recalculates the whole layout, then continues. Your JavaScript waits. Your animation freezes. Your user notices.

What Is a Repaint

A repaint happens when something visual changes but the layout does not. For example: changing a colour, changing a background image, or changing the visibility of an element. The browser does not need to recalculate positions — it just needs to redraw the pixels.

Repaints are cheaper than reflows but they are still not free. Every repaint takes time, and many repaints per second add up quickly.

⚠️ Important rule: every reflow is always followed by a repaint. But a repaint does not always cause a reflow. So when you are optimising, the biggest wins come from reducing reflows.

What Triggers Reflows and Repaints

Here are the most common things that trigger a reflow. Knowing this list is the first step to avoiding them:

JavaScript — things that trigger a reflow
// READING layout properties forces the browser to reflow first // so the value it gives you is up to date el.offsetWidth // triggers reflow el.offsetHeight // triggers reflow el.offsetTop // triggers reflow el.getBoundingClientRect() // triggers reflow el.scrollHeight // triggers reflow el.clientWidth // triggers reflow window.getComputedStyle(el) // triggers reflow // WRITING layout properties causes a reflow on next paint el.style.width = '200px' // causes reflow el.style.height = '100px' // causes reflow el.style.padding = '10px' // causes reflow el.style.margin = '5px' // causes reflow el.style.top = '50px' // causes reflow el.appendChild(child) // causes reflow el.removeChild(child) // causes reflow // These cause repaint only (cheaper) el.style.color = 'red' // repaint only el.style.background = 'blue' // repaint only el.style.visibility= 'hidden' // repaint only // These cause neither (cheapest) el.style.transform = 'translateX(10px)' // GPU only el.style.opacity = '0.5' // GPU only

Read Then Write — Never Mix Them

The single most important rule in DOM optimisation is this: do all your reads first, then do all your writes. Never mix reading and writing layout properties in the same loop.

Here is why this matters so much. When you write a layout property, the browser marks the layout as "dirty" — meaning it needs to be recalculated. It does not recalculate immediately though. It waits until it needs to. But the moment you read a layout property, the browser is forced to recalculate right now so it can give you the correct current value. If you keep alternating between reads and writes, you are triggering a full layout recalculation on every single line.

JavaScript — the bad pattern vs the good pattern
// BAD — alternating reads and writes, causes 3 separate reflows box1.style.width = box2.offsetWidth + 'px'; // write → read → reflow box2.style.width = box3.offsetWidth + 'px'; // write → read → reflow box3.style.width = box1.offsetWidth + 'px'; // write → read → reflow // GOOD — all reads first, then all writes, causes only 1 reflow const w1 = box1.offsetWidth; // read const w2 = box2.offsetWidth; // read const w3 = box3.offsetWidth; // read box1.style.width = w2 + 'px'; // write box2.style.width = w3 + 'px'; // write box3.style.width = w1 + 'px'; // write
The simple rule: store all the measurements you need in variables first. Then apply all the changes. Never read a measurement after you have already written a style change in the same block of code.

Batching DOM Updates

When you need to add many elements to the page at once — like rendering a list of 100 items — doing it one element at a time is extremely slow. Each appendChild triggers a reflow and a repaint. That is 100 reflows for 100 items.

The solution is to build all your elements in memory first, then add them to the page in one single operation. The browser only reflows once.

DocumentFragment

A DocumentFragment is like a temporary invisible container that lives in memory, not in the actual page. You can add as many elements to it as you like without triggering any reflows. Then when you are done, you attach the whole fragment to the page in one go.

JavaScript — DocumentFragment batches 100 appends into 1
// BAD — 100 appends = 100 reflows const list = document.getElementById('myList'); for (let i = 0; i < 100; i++) { const li = document.createElement('li'); li.textContent = `Item ${i + 1}`; list.appendChild(li); // reflow on every iteration } // GOOD — build in memory, attach once = 1 reflow const list = document.getElementById'myList'); const fragment = document.createDocumentFragment(); for (let i = 0; i < 100; i++) { const li = document.createElement('li'); li.textContent = `Item ${i + 1}`; fragment.appendChild(li); // no reflow, just memory } list.appendChild(fragment); // one single reflow at the end

Building with innerHTML in One Shot

Another very effective pattern is building your entire HTML as a string and setting innerHTML once. This causes a single parse and a single reflow regardless of how many elements are in the string.

JavaScript — build as a string, set innerHTML once
// Build the entire list as a string first const items = ['Apple', 'Banana', 'Cherry', 'Mango']; const html = items .map(item => `<li class="fruit-item">${item}</li>`) .join(''); // Set it all in one go — 1 reflow only document.getElementById('fruitList').innerHTML = `<ul>${html}</ul>`; // This is much faster than appending each <li> one at a time
⚠️ Safety note with innerHTML: never put user-typed content directly into an innerHTML string without sanitising it first. If a user types <script>...</script> into a text box and you put it straight into innerHTML it will run. Always clean user input before using it this way.

Change CSS Classes, Not Individual Styles

If you need to apply several style changes to an element at once, do not set each one separately. Every el.style.something = value is another potential reflow trigger. Instead, define a CSS class with all those styles, then just toggle the class name.

JavaScript and CSS — toggling a class is far better
// BAD — 5 separate style changes, browser could reflow multiple times el.style.width = '300px'; el.style.height = '200px'; el.style.padding = '20px'; el.style.background = '#1a1b26'; el.style.borderRadius= '12px'; // GOOD — add the CSS class, browser applies everything together el.classList.add('card-expanded'); /* In your CSS file: */ /* .card-expanded { */ /* width: 300px; */ /* height: 200px; */ /* padding: 20px; */ /* background: #1a1b26; */ /* border-radius: 12px; */ /* } */ // You can also toggle back and forth cleanly el.classList.toggle('card-expanded'); el.classList.remove('card-expanded');

Layout Thrashing — The Worst Offender

Layout thrashing is when you mix reads and writes in a loop. It forces the browser to do a full layout recalculation on every single iteration. This is the most common performance mistake beginners make.

JavaScript — layout thrashing vs fixing it
// LAYOUT THRASHING — causes a reflow on every loop iteration const boxes = document.querySelectorAll('.box'); boxes.forEach(box => { const width = box.offsetWidth; // READ → forces reflow box.style.width = width * 2 + 'px'; // WRITE → invalidates layout // next iteration reads again → forces ANOTHER reflow }); // FIXED — all reads first, then all writes const boxes = document.querySelectorAll('.box'); // Pass 1: read all measurements const widths = [...boxes].map(box => box.offsetWidth); // Pass 2: apply all writes boxes.forEach((box, i) => { box.style.width = widths[i] * 2 + 'px'; }); // Now there is only 1 reflow total, no matter how many boxes

requestAnimationFrame — Do Visual Work at the Right Time

requestAnimationFrame (usually shortened to rAF) tells the browser: "run this code right before you paint the next frame." This is the right place to do any DOM changes that need to animate smoothly, because the browser can optimise and batch them properly.

Think of it like a chef at a restaurant. Instead of running to the customer with each ingredient as you prepare it, you wait until the whole dish is ready and bring everything out at once. rAF is the "one trip" approach for DOM updates.

JavaScript — requestAnimationFrame for smooth animations
// BAD — using setInterval, may run at wrong time in the frame setInterval(() => { el.style.left = position++ + 'px'; // not synced with browser paint }, 16); // GOOD — synced with the browser's own paint cycle let position = 0; function animate() { position++; el.style.transform = `translateX(${position}px)`; if (position < 400) { requestAnimationFrame(animate); // schedule next frame } } requestAnimationFrame(animate); // kick it off // rAF automatically pauses when the tab is hidden — saves battery // rAF gives you a timestamp you can use for time-based animation function animate(timestamp) { const progress = timestamp / 1000; // seconds since page load el.style.transform = `translateX(${Math.sin(progress) * 100}px)`; requestAnimationFrame(animate); }

Use transform and opacity for All Animations

This is the most impactful rule for smooth animations. When you animate transform or opacity, the browser can handle it entirely on the GPU without touching the Layout or Paint steps at all. Everything else — top, left, margin, width, height — causes a reflow or repaint on every frame.

CSS and JavaScript — GPU accelerated vs slow animations
/* SLOW — causes reflow on every frame of the animation */ .slide-bad { transition: left 0.3s ease; /* triggers reflow every frame */ transition: margin-left 0.3s ease; /* triggers reflow every frame */ transition: width 0.3s ease; /* triggers reflow every frame */ } /* FAST — GPU handles this, zero reflows or repaints */ .slide-good { transition: transform 0.3s ease; /* GPU accelerated */ } .fade-good { transition: opacity 0.3s ease; /* GPU accelerated */ } // In JavaScript — move elements with transform not position // Slow: el.style.left = '200px'; // Fast: el.style.transform = 'translateX(200px)'; // Fade out slowly: el.style.opacity = '0'; // GPU — no reflow, no repaint

The will-change CSS Hint

You can give the browser a heads-up about which elements are going to animate. When the browser knows an element will be transformed, it can promote it to its own GPU layer in advance so the animation is completely isolated from the rest of the page.

CSS — will-change promotes an element to its own layer
/* Tell the browser this element will animate */ .animated-card { will-change: transform; /* hint: this element transforms soon */ } .fading-overlay { will-change: opacity; /* hint: this element fades soon */ } /* You can also add it in JavaScript just before the animation starts */ /* and remove it after the animation ends */ el.style.willChange = 'transform'; // before animation // ... run your animation ... el.style.willChange = 'auto'; // after animation — free the memory
⚠️ Do not overuse will-change. Each element with will-change gets its own GPU layer, which uses extra video memory. If you apply it to too many elements at once you can actually make performance worse. Only use it on elements that are genuinely about to animate.

Quick Reference Table

Operation Cost Why
Animate with transform None Handled by GPU, no layout step
Animate opacity None Handled by GPU, no layout step
Change color or background Repaint Pixels redraw but layout stays same
Change width, height or padding Reflow Browser recalculates all positions
Animate left or top Reflow Triggers full layout on every frame
Read offsetWidth in a loop Reflow Forces layout sync before each read
classList.add one class One reflow Browser batches all rule changes
appendChild in a loop Many reflows Each insert triggers layout
DocumentFragment then append One reflow All changes land at once

⚡ Key Takeaways
  • The browser renders pages in steps: Style, Layout, Paint, Composite. Layout and Paint are expensive. Anything that triggers them costs time.
  • A reflow happens when the browser must recalculate element sizes and positions. It is the most expensive operation and always followed by a repaint.
  • A repaint happens when visuals change but layout stays the same (like a colour change). It is cheaper than a reflow but still has a cost.
  • Read all layout properties first, then write. Never mix reads and writes in a loop. This single rule eliminates most layout thrashing.
  • When adding many elements, use DocumentFragment or build an HTML string and set innerHTML once. This reduces many reflows down to one.
  • Toggle CSS class names instead of setting individual style properties one by one. The browser applies all changes at once.
  • Use transform and opacity for all animations. They run entirely on the GPU and cause zero reflows or repaints.
  • Use requestAnimationFrame for any visual updates. It syncs your code with the browser paint cycle and pauses automatically when the tab is not visible.
  • Add will-change: transform on elements that are about to animate, but remove it straight after to free up GPU memory.