Suggest an editImprove this articleRefine the answer for “Batching DOM changes”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)The goal is to **touch the DOM and layout less**, and when you do, do it **in batches**: all the reads (measure) first, then all the writes (mutate), and no more than once per frame. **Key point:** interleaving geometry reads and writes causes layout thrashing, while batching through `requestAnimationFrame` and `DocumentFragment` keeps reflows to a minimum.Shown above the full answer for quick recall.Answer (EN)ImageIn 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)`; }); ```For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.