Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Групування змін DOM». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Мета проста: чіпати DOM і layout якомога рідше, а якщо вже чіпати, то пакетно: спочатку всі читання (measure), потім усі записи (mutate), і не частіше ніж раз на кадр.** Читання `getBoundingClientRect()`, `offsetHeight`, `scrollTop`, `getComputedStyle()` змушують браузер синхронно перерахувати layout, тому чергування читань і записів у циклі дає layout thrashing. Записи коалесцюють через `requestAnimationFrame`, нові вузли збирають у `DocumentFragment` або `<template>` і вставляють одним рухом, десяток інлайнових стилів замінюють одним `classList.add()`, анімують лише `transform` і `opacity`, а довгі списки віртуалізують. ```javascript const rects = items.map((el) => el.getBoundingClientRect()); // усі читання requestAnimationFrame(() => { items.forEach((el, i) => { el.style.transform = `translateY(${rects[i].top}px)`; // усі записи }); }); ``` **Ключове:** розділення фаз read і write в один кадр перетворює N примусових layout на один.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Групування змін 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, і лише потім зміни в коді.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.