Skip to main content

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:

javascript
// 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).

javascript
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
javascript
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>:
javascript
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.

javascript
el.classList.add('expanded'); // all the properties are already described in CSS

5) "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.

javascript
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).

javascript
.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.
javascript
.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.

javascript
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:

javascript
// 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)

javascript
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:

javascript
read(() => { boxRect = box.getBoundingClientRect(); }); write(() => { box.style.transform = `translateY(${boxRect.top}px)`; });

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.