Batching DOM changes
In short: the goal is to touch the DOM and layout less, and when you do, to do it in batches: all the reads (measure) first, then all the writes (mutate), and no more than once per frame.
Practical ways to batch DOM changes
1) "Measure -> then Mutate" (separate reads from writes)
Reads (getBoundingClientRect, offsetHeight, scrollTop, getComputedStyle) force the browser to synchronously recompute layout. If you interleave reads and writes, you get layout thrashing.
Correct:
// Collect all the geometry reads first…
const rects = items.map(el => el.getBoundingClientRect());
// …then apply the writes (styles/classes) in one batch
requestAnimationFrame(() => {
items.forEach((el, i) => {
el.style.transform = `translateY(${rects[i].top}px)`; // write only
});
});2) Batching via requestAnimationFrame
rAF coalesces changes into a single frame (~16.6 ms at 60 FPS).
let pending = [];
function batchStyle(fn) {
pending.push(fn);
if (pending.length === 1) {
requestAnimationFrame(() => {
const jobs = pending; pending = [];
jobs.forEach(job => job());
});
}
}
// Usage
batchStyle(() => el.classList.add('active'));
batchStyle(() => el.style.opacity = '1');3) Build nodes "off to the side", then insert them all at once
- DocumentFragment
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Row ${i}`;
frag.appendChild(li);
}
list.appendChild(frag); // one reflow instead of 1000<template>:
const tpl = document.getElementById('row-tpl');
const frag = document.createDocumentFragment();
data.forEach(d => {
const node = tpl.content.cloneNode(true);
node.querySelector('.title').textContent = d.title;
frag.appendChild(node);
});
list.appendChild(frag);innerHTML(when it's safe): build a string and insert it once.
4) Toggle a class instead of dozens of inline styles
A single classList.add('expanded') is cheaper than 10 style.* assignments.
el.classList.add('expanded'); // all the properties are already described in CSS5) "Hide -> change -> show"
Sometimes it pays to take a block out of the flow (reducing its impact on layout), make a batch of edits, and then bring it back.
container.style.display = 'none';
/* lots of DOM operations */
container.style.display = '';Use carefully (it can reset sub-measurements, focus, and so on).
6) Animate only transform and opacity
These don't trigger reflow, usually only repaint/composite (cheaper).
.card { will-change: transform, opacity; }7) CSS containment and skipping rendering
content-visibility: auto;: skips layout/paint for invisible content.contain: layout paint;: limits the area recomputed.
.section {
content-visibility: auto;
contain-intrinsic-size: 1000px; /* so layout doesn't jump */
}8) Debounce/throttle events
Scroll/resize/input events can trigger hundreds of updates per second, so batch them.
const onScroll = throttle(() => {
requestAnimationFrame(updateStickyHeader);
}, 100);9) Virtualize large lists
Render only the visible elements (react-window, virtual-scroller, or your own solution). This radically reduces the DOM's size, meaning less layout/paint.
10) Minimize "forced reflow"
Read everything you need first:
// bad: write -> read -> write -> read …
el.style.width = '400px';
el.offsetHeight; // forced reflow
el.style.height = '200px';
el.getBoundingClientRect(); // forced reflow
// good: all the reads, then all the writes
const h = el.offsetHeight;
const r = el.getBoundingClientRect();
requestAnimationFrame(() => {
el.style.width = '400px';
el.style.height = '200px';
});A "measure / mutate" mini-template (ready to copy-paste)
const reads = [];
const writes = [];
export function read(fn) {
reads.push(fn);
schedule();
}
export function write(fn) {
writes.push(fn);
schedule();
}
let scheduled = false;
function schedule() {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
// 1) all the reads
let r; while ((r = reads.shift())) r();
// 2) all the writes
let w; while ((w = writes.shift())) w();
scheduled = false;
});
}Usage:
read(() => { boxRect = box.getBoundingClientRect(); });
write(() => { box.style.transform = `translateY(${boxRect.top}px)`; });Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.