DEV Community

Greatness
Greatness

Posted on

Advanced DOM Performance: Stop Causing Unnecessary Reflows in Plain JavaScript

Most web performance issues in client-side applications don't stem from large bundle sizes—they happen inside the browser render loop. Every time your JavaScript reads a layout property immediately after altering the DOM, you force the browser into a costly synchronous calculation known as forced synchronous layout (or layout thrashing).
​Understanding how the render pipeline handles layout recalculations is crucial for writing snappy, responsive user interfaces without relying on virtual DOM abstractions.

​What Causes a Forced Reflow?
​The browser batches layout changes for performance. However, if you write code that mutates the DOM and then immediately requests geometric properties (like offsetHeight, getBoundingClientRect(), or scrollTop), the browser is forced to flush its queue and recalculate the layout instantly.

JavaScript code:
// ❌ BAD: Layout Thrashing inside a loop
const cards = document.querySelectorAll('.card');

cards.forEach(card => {
// Reading offsetHeight forces the browser to recalculate layout on EVERY iteration!
const height = card.offsetHeight;
card.style.height = ${height + 10}px;
// Mutation
});

The Solution:
Batch Reads and Writes Separately
To prevent thrashing, structure your DOM operations into two distinct phases: read all layout measurements first, then perform all writes together.

JavaScript code:
// ✅ GOOD: Separating Reads from Writes
const cards = document.querySelectorAll('.card');

// Phase 1: Read measurements
const heights = Array.from(cards).map(card => card.offsetHeight);

// Phase 2: Write mutations
cards.forEach((card, index) => {
card.style.height = ${heights[index] + 10}px;
});

Using requestAnimationFrame for Smooth UI Updates
When handling high-frequency events like scrolling or resize listeners, offload DOM mutations to the browser's native frame render window using requestAnimationFrame.

JavaScript code:
let isTicking = false;

window.addEventListener('scroll', () => {
if (!isTicking) {
window.requestAnimationFrame(() => {
updateProgressBar();
isTicking = false;
});
isTicking = true;
}
});

function updateProgressBar() {
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const progress = (scrollTop / scrollHeight) * 100;

document.querySelector('#progress-bar').style.width = ${progress}%;
}

Key takeaways
1.Never interleave DOM geometry reads with style writes inside loops.

  1. Batch layout reads first, then execute batch writes. Wrap scroll and UI state mutations inside 3.requestAnimationFrame to align with display refresh cycles.

"Building a SaaS platform or dev tool and need clear, practical technical documentation or engineering guides for your users? I specialize in writing clean, human-crafted developer content. Let's connect!"

Top comments (0)