Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Групування змін DOM». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)Мета - **менше "чіпати" DOM і layout**, а якщо вже чіпати - робити це **пакетно**: спочатку всі читання (measure), потім усі записи (mutate), і не частіше одного кадру. **Ключове:** чергування читань і записів геометрії викликає layout thrashing, тоді як групування через `requestAnimationFrame` та `DocumentFragment` зводить кількість reflow до мінімуму.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)ЗображенняКоротко: мета - **менше "чіпати" 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)`; }); ```Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.