Skip to main content

Групування змін DOM

Групування змін DOM, це підхід, за якого всі читання геометрії роблять разом, потім разом роблять усі записи, і все це вкладається в один кадр. Так браузер перераховує layout один раз замість десятків разів, а інтерфейс перестає гальмувати на списках, скролі та анімаціях.

Теорія

TL;DR

  • Спочатку всі читання (measure), потім усі записи (mutate), не частіше ніж раз на кадр.
  • Читання getBoundingClientRect(), offsetHeight, scrollTop, getComputedStyle() викликають синхронний layout; чергування читань і записів дає layout thrashing.
  • requestAnimationFrame коалесцює записи в один кадр (близько 16.6 мс при 60 FPS).
  • Нові вузли збирайте у DocumentFragment або з <template> і вставляйте однією операцією.
  • Один classList.add() дешевший за десяток присвоєнь style.*.
  • Анімуйте лише transform і opacity, обмежуйте перерахунок через contain і content-visibility, а великі списки віртуалізуйте.

Швидкий приклад

javascript
// Погано: write, read, write, read, кожне читання форсує layout el.style.width = '400px'; el.offsetHeight; // forced reflow el.style.height = '200px'; el.getBoundingClientRect(); // forced reflow // Добре: спочатку всі читання, потім усі записи в одному кадрі const h = el.offsetHeight; const r = el.getBoundingClientRect(); requestAnimationFrame(() => { el.style.width = '400px'; el.style.height = '200px'; });

Measure, потім mutate

Читання геометрії (getBoundingClientRect, offsetHeight, scrollTop, getComputedStyle) змушують браузер синхронно перерахувати layout, бо він мусить віддати вам актуальне значення. Якщо чергувати читання і запис, ви отримаєте layout thrashing: кожна ітерація циклу коштує повного перерахунку.

Правильний порядок:

javascript
// Збираємо все читання геометрії... const rects = items.map((el) => el.getBoundingClientRect()); // ...а потім однією пачкою застосовуємо записи (стилі, класи) requestAnimationFrame(() => { items.forEach((el, i) => { el.style.transform = `translateY(${rects[i].top}px)`; // лише запис }); });

Бетчинг через requestAnimationFrame зводить усі зміни до одного кадру:

javascript
let pending = []; function batchStyle(fn) { pending.push(fn); if (pending.length === 1) { requestAnimationFrame(() => { const jobs = pending; pending = []; jobs.forEach((job) => job()); }); } } // Використання batchStyle(() => el.classList.add('active')); batchStyle(() => (el.style.opacity = '1'));

Будувати вузли поза документом

DocumentFragment живе поза деревом документа, тож наповнення його нічого не коштує, а вставка робить один 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); // один reflow замість 1000

Те саме з <template>, коли розмітка рядка описана в 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);

Третій варіант, innerHTML, коли це безпечно: зібрати рядок і вставити його один раз. Важливо: вміст, що приходить від користувача, так вставляти не можна, це XSS.

Менше записів і менший обсяг DOM

Клас замість десятка інлайнових стилів. Один classList.add('expanded') дешевший за 10 присвоєнь style.*, бо це одна зміна для рушія стилів.

javascript
el.classList.add('expanded'); // всі властивості вже описані в CSS

Сховати, змінити, показати. Інколи вигідно виключити блок з потоку, внести пачку правок і повернути його назад.

javascript
container.style.display = 'none'; /* багато DOM-операцій */ container.style.display = '';

Використовуйте обережно: це скидає проміжні вимірювання, позицію скролу і фокус усередині блоку.

Анімуйте лише transform і opacity. Вони не спричиняють reflow, зазвичай це лише repaint і composite, тобто дешево.

css
.card { will-change: transform, opacity; }

CSS-контейнери і пропуск рендера. content-visibility: auto дозволяє браузеру пропускати layout і paint для невидимого контенту, а contain: layout paint обмежує область перерахунку межами елемента.

css
.section { content-visibility: auto; contain-intrinsic-size: 1000px; /* щоб layout не стрибав */ }

Debounce і throttle подій. Scroll, resize та введення тексту можуть тригерити сотні апдейтів на секунду, тож збирайте їх у пачки.

javascript
const onScroll = throttle(() => { requestAnimationFrame(updateStickyHeader); }, 100);

Віртуалізація великих списків. Рендерте лише видимі елементи (react-window, virtual scroller або власна реалізація). Це радикально скорочує обсяг DOM, а отже і роботу layout та paint.

Готовий шаблон measure / mutate

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) усі читання let r; while ((r = reads.shift())) r(); // 2) усі записи let w; while ((w = writes.shift())) w(); scheduled = false; }); }

Використання:

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

Типові помилки

  • Читати геометрію всередині циклу, який одразу ж пише стилі. Це і є layout thrashing, і він найдорожчий з усього переліченого.
  • Вставляти вузли по одному в document.body замість того, щоб зібрати їх у фрагмент.
  • Вважати, що requestAnimationFrame сам по собі прискорює код. Він лише переносить роботу в потрібну фазу кадру; якщо всередині колбека чергувати читання і записи, thrashing нікуди не подінеться.
  • Ставити will-change на все підряд: кожен шар композиції коштує пам'яті, надлишкові шари гальмують сторінку.
  • Використовувати display: none як універсальний трюк, забуваючи, що він скидає скрол, фокус і стан програвання медіа всередині.
  • Вставляти користувацький рядок через innerHTML: це пряма XSS-вразливість, а не оптимізація.
  • Оптимізувати наосліп. Спершу запис у вкладці Performance, пошук довгих Layout і Recalculate Style, і лише потім зміни в коді.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.