Suggest an editImprove this articleRefine the answer for “Batching DOM updates”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**The goal is simple: touch the DOM and the layout as rarely as possible, and when you do touch them, do it in batches: all reads first (measure), then all writes (mutate), and no more than once per frame.** Reading `getBoundingClientRect()`, `offsetHeight`, `scrollTop` or `getComputedStyle()` forces the browser into a synchronous layout, so interleaving reads and writes inside a loop produces layout thrashing. Writes are coalesced with `requestAnimationFrame`, new nodes are assembled in a `DocumentFragment` or a `<template>` and inserted in one go, a dozen inline styles are replaced by a single `classList.add()`, only `transform` and `opacity` are animated, and long lists are virtualized. ```javascript const rects = items.map((el) => el.getBoundingClientRect()); // all reads requestAnimationFrame(() => { items.forEach((el, i) => { el.style.transform = `translateY(${rects[i].top}px)`; // all writes }); }); ``` **Key point:** splitting the read and write phases into one frame turns N forced layouts into one.Shown above the full answer for quick recall.Answer (EN)Image**Batching DOM updates is the practice of doing all geometry reads together, then all writes together, and fitting the whole thing into a single frame.** That way the browser recalculates layout once instead of dozens of times, and the interface stops stuttering on lists, scrolling and animations. ## Theory ### TL;DR - All reads first (measure), then all writes (mutate), no more than once per frame. - Reading `getBoundingClientRect()`, `offsetHeight`, `scrollTop` or `getComputedStyle()` triggers a synchronous layout; interleaving reads and writes produces layout thrashing. - `requestAnimationFrame` coalesces writes into one frame (about 16.6 ms at 60 FPS). - Build new nodes in a `DocumentFragment` or from a `<template>` and insert them in a single operation. - One `classList.add()` is cheaper than a dozen `style.*` assignments. - Animate only `transform` and `opacity`, bound the recalculation with `contain` and `content-visibility`, and virtualize long lists. ### Quick example ```javascript // Bad: write, read, write, read, every read forces a layout el.style.width = '400px'; el.offsetHeight; // forced reflow el.style.height = '200px'; el.getBoundingClientRect(); // forced reflow // Good: all reads first, then all writes in one frame const h = el.offsetHeight; const r = el.getBoundingClientRect(); requestAnimationFrame(() => { el.style.width = '400px'; el.style.height = '200px'; }); ``` ### Measure, then mutate Geometry reads (`getBoundingClientRect`, `offsetHeight`, `scrollTop`, `getComputedStyle`) force the browser to recalculate layout synchronously, because it has to hand you an up to date value. Interleave reads and writes and you get layout thrashing: every loop iteration costs a full recalculation. The right order: ```javascript // Collect all the geometry reads... const rects = items.map((el) => el.getBoundingClientRect()); // ...and then apply the writes (styles, classes) in one batch requestAnimationFrame(() => { items.forEach((el, i) => { el.style.transform = `translateY(${rects[i].top}px)`; // write only }); }); ``` Batching through `requestAnimationFrame` collapses all the changes into one frame: ```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')); ``` ### Build nodes outside the document A `DocumentFragment` lives outside the document tree, so filling it costs nothing and the insertion causes a single reflow: ```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 ``` The same with a `<template>` when the row markup is described in HTML: ```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); ``` A third option, `innerHTML`, when it is safe: build a string and insert it once. Important: content coming from users must never be inserted this way, that is an XSS hole. ### Fewer writes and a smaller DOM **A class instead of a dozen inline styles.** One `classList.add('expanded')` is cheaper than 10 `style.*` assignments, because it is a single change for the style engine. ```javascript el.classList.add('expanded'); // all the properties are already described in CSS ``` **Hide, change, show.** Sometimes it pays to take a block out of the flow, apply a batch of edits and put it back. ```javascript container.style.display = 'none'; /* many DOM operations */ container.style.display = ''; ``` > Use it carefully: it discards intermediate measurements, the scroll position and the focus inside the block. **Animate only `transform` and `opacity`.** They do not cause a reflow, usually only a repaint and a composite, which is cheap. ```css .card { will-change: transform, opacity; } ``` **CSS containment and skipped rendering.** `content-visibility: auto` lets the browser skip layout and paint for content that is off screen, and `contain: layout paint` limits the recalculation to the element's own box. ```css .section { content-visibility: auto; contain-intrinsic-size: 1000px; /* so the layout does not jump */ } ``` **Debounce and throttle events.** Scroll, resize and typing can trigger hundreds of updates per second, so collect them into batches. ```javascript const onScroll = throttle(() => { requestAnimationFrame(updateStickyHeader); }, 100); ``` **Virtualize large lists.** Render only the visible items (`react-window`, a virtual scroller or your own implementation). This cuts the DOM size radically, and with it the layout and paint work. ### A ready made measure / mutate scheduler ```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 reads let r; while ((r = reads.shift())) r(); // 2) all writes let w; while ((w = writes.shift())) w(); scheduled = false; }); } ``` Usage: ```javascript read(() => { boxRect = box.getBoundingClientRect(); }); write(() => { box.style.transform = `translateY(${boxRect.top}px)`; }); ``` ### Common mistakes - Reading geometry inside a loop that immediately writes styles. This is layout thrashing, and it is the most expensive mistake on this list. - Appending nodes one by one to `document.body` instead of collecting them in a fragment. - Assuming `requestAnimationFrame` speeds up code by itself. It only moves the work into the right phase of the frame; if the callback still interleaves reads and writes, the thrashing stays. - Putting `will-change` on everything: every compositing layer costs memory, and redundant layers slow the page down. - Using `display: none` as a universal trick, forgetting that it resets scroll, focus and media playback state inside. - Inserting a user supplied string through `innerHTML`: that is a direct XSS vulnerability, not an optimization. - Optimizing blind. First record in the Performance tab, look for long Layout and Recalculate Style entries, and only then change the code.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.