Skip to main content

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

Коротко: мета - менше "чіпати" DOM і layout, а якщо вже чіпати - робити це пакетно: спочатку всі читання (measure), потім усі записи (mutate), і не частіше одного кадру.


Практичні способи групування змін DOM

1) "Measure -> then 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)`; // тільки запис }); });

2) Бетчинг через requestAnimationFrame

rAF коалесує зміни в один кадр (~16.6 мс при 60 FPS).

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');

3) Будувати вузли "осторонь", а потім одним вставити

  • 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); // один reflow замість 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 (коли безпечно): зібрати рядок і вставити один раз.

4) Змінюй клас замість десятків інлайнових стилів

Один classList.add('expanded') дешевший, ніж 10 style.*.

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

5) "Сховати -> змінити -> показати"

Іноді вигідно виключити блок з потоку (зменшити вплив на layout), внести пачку правок, а потім повернути.

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

Використовуй акуратно (може скинути суб-виміри, фокуси тощо).

6) Анімуй тільки transform і opacity

Вони не викликають reflow, зазвичай лише repaint/composite (дешевше).

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

7) CSS-контейнери та пропуски рендера

  • content-visibility: auto; - пропускає layout/paint невидимого контенту.
  • contain: layout paint; - обмежує область перерахунку.
javascript
.section { content-visibility: auto; contain-intrinsic-size: 1000px; /* щоб не стрибав layout */ }

8) Дебаунс/тротлінг подій

Скрол/резайз/інпути можуть тригерити сотні апдейтів на секунду - збирай їх у пачки.

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

9) Віртуалізація великих списків

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

10) Мінімізуй "forced reflow"

Спочатку прочитай усе потрібне:

javascript
// погано: write -> read -> write -> read … 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" (готовий до копіпасти)

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)`; });

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

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

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