Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Коли відбуваються reflow та repaint у браузері». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Reflow** відбувається, коли браузер перераховує геометрію елементів після змін, що впливають на layout. **Repaint** перемальовує пікселі без зміни розміщення елементів. ```js box.style.background = 'blue'; // тільки repaint box.style.width = '200px'; // reflow + repaint ``` **Головне:** кожен reflow викликає repaint. Для анімацій використовуй `transform` та `opacity`, щоб уникнути обох.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Reflow** відбувається, коли браузер перераховує геометрію елементів після змін, що впливають на layout. **Repaint** перемальовує пікселі для візуальних змін, які нічого не зміщують. ## Теорія ### TL;DR - Reflow схожий на перестановку меблів: коли один предмет зміщується, решта теж можуть зрушити - Repaint схожий на перефарбовування стіни: нічого не рухається, змінюються тільки кольори пікселів - Кожен reflow викликає repaint, але repaint не викликає reflow - Зміна `width` або `height` = reflow + repaint. Зміна кольору = тільки repaint - Анімації через `transform` та `opacity` можуть обійти обидва процеси (compositor-only) ### Швидкий приклад ```html <div id="box" style="width:100px; height:100px; background:red; margin:20px;"></div> <div id="sibling">Сусідній елемент</div> <script> const box = document.getElementById('box'); // Тільки repaint: колір не впливає на геометрію, сусід залишається на місці box.style.background = 'blue'; // Reflow + repaint: зміна висоти зміщує сусідній елемент вниз box.style.height = '200px'; </script> ``` Зміна кольору швидка: браузер перемальовує тільки пікселі. Зміна висоти запускає повний перерахунок layout: браузер піднімається до батька, потім спускається до сусідів і оновлює координати всіх залежних елементів. ### Як браузер обробляє зміни Браузер будує render tree з DOM та CSSOM. Під час layout (reflow) рушій призначає кожному вузлу точні пікселі та розміри за допомогою formatting contexts. Результат растеризується на екран через GPU у Blink/Chromium або через CPU у Gecko. Коли щось змінюється, браузер позначає залежні вузли як "dirty". На наступному paint-фреймі (приблизно кожні 16ms при 60fps) він перераховує layout-піддерево від зміненого вузла вгору до предків і вниз до нащадків та сусідів. Саме це поширення робить reflow дорогим на складних сторінках. Repaint пропускає весь геометричний етап. Рушій перерастеризує тільки пікселі для зміненої візуальної властивості. Сусідні елементи не зачіпаються. ### Головна різниця Reflow перебудовує геометрію від зміненого вузла назовні: батьки, діти та сусіди можуть усі зміститися. Repaint оновлює тільки пікселі самого елемента. Тому `display: none` викликає reflow (елемент виходить з document flow, простір стискається), а `visibility: hidden` викликає тільки repaint (простір залишається, нічого не зміщується). ### Коли відбувається reflow - Додавання або видалення DOM-елементів - Зміна `width`, `height`, `margin`, `padding`, `border-width` - Зміна `position`, `top`, `left`, `float` - Зміна `font-size`, `font-family`, `line-height` - Перемикання `display` між `none` та `block` - Зміна розміру вікна браузера - Читання layout-властивостей: `offsetWidth`, `offsetHeight`, `getBoundingClientRect()` Останній пункт часто дивує розробників. Читання layout-властивості в середині фрейму змушує браузер негайно скинути чергу "dirty"-вузлів та перерахувати layout, навіть якщо це відбувається під час анімації. ### Коли відбувається repaint (без reflow) - Зміна `color`, `background-color`, `background-image` - Зміна `border-color`, `box-shadow`, `outline` - Перемикання `visibility` (не `display`) - Зміна `opacity` на некомпозитованих елементах ### Примусовий синхронний reflow ```html <div id="anim" style="width:100px; transition:width 1s; background:blue;"></div> <script> const el = document.getElementById('anim'); el.style.width = '200px'; // додає вузол до черги "dirty", reflow ще не виконано // Читання offsetWidth тут змушує браузер негайно скинути чергу // Це руйнує плавність анімації console.log(el.offsetWidth); // примусовий синхронний reflow // Рішення: читати layout-властивості до будь-яких записів у тому ж фреймі </script> ``` У Chrome DevTools це видно як жовті спайки "Layout" у flame chart вкладки Performance. `useLayoutEffect` у React навмисно запускається після reflow і до repaint, саме тому він блокує відмальовування. ### Типові помилки **Читання та запис у циклі** ```js // Погано: кожне читання offsetTop скидає чергу - N reflow для N елементів for (let i = 0; i < elements.length; i++) { elements[i].style.top = elements[i].offsetTop + 10 + 'px'; } // Правильно: спочатку всі читання, потім всі записи const tops = elements.map(el => el.offsetTop); // один reflow elements.forEach((el, i) => { el.style.top = tops[i] + 10 + 'px'; // тільки записи }); ``` **Анімація layout-властивостей** ```js // Погано: reflow на кожному фреймі анімації element.style.left = currentX + 'px'; // Правильно: compositor обробляє це без reflow element.style.transform = `translateX(${currentX}px)`; ``` **Таблиці з динамічними рядками** ```css /* Додавання одного рядка перераховує всі комірки таблиці */ table { display: table; } /* Рішення: flexbox або grid для динамічних списків */ .list { display: flex; flex-direction: column; } ``` **Завантаження шрифту без font-display** ```css /* Без font-display браузер ховає текст до завантаження шрифту, а потім викликає reflow при його появі */ @font-face { src: url(font.woff2); font-display: swap; /* запобігає layout shift при завантаженні */ } ``` ### Де зустрічається в реальних проектах - **React**: `useLayoutEffect` запускається після reflow і до repaint для DOM-вимірювань, використовується в react-window для розрахунків віртуальних списків - **GSAP**: `xPercent` та `scaleX` не викликають reflow, на відміну від анімації `left` або `width` - **Swiper.js**: `contain: layout` на контейнерах слайдів ізолює reflow каруселі від решти сторінки - **Chart.js**: рендеринг через `<canvas>` повністю обходить DOM reflow для анімацій графіків Я якось налагоджував карусель, де читання `offsetWidth` всередині циклу анімації давало 60 reflow на секунду. Перенесення читання за межі циклу скоротило час layout з ~60ms до менш ніж 2ms на фрейм. ### Питання на співбесіді **Q:** Яка різниця між reflow, repaint та compositing? **A:** Compositing змішує вже відмальовані шари на GPU без будь-якого звернення до DOM. `transform` та `opacity` на promoted-шарах тільки компонуються. Reflow відповідає за геометрію, repaint за пікселі, compositing просто змішує шари. **Q:** Як виміряти reflow у браузері? **A:** Відкрий Chrome DevTools, перейди до вкладки Performance, запиши дію та шукай події "Layout" довше 1ms. "Recalculate Style" це робота з CSSOM, а не reflow. **Q:** Чи не бере участь у reflow елемент з `position: fixed`? **A:** Ні, fixed-елементи все одно викликають reflow. `contain: layout` на компоненті справді ізолює поширення reflow. **Q:** Як CSS `contain` впливає на поширення reflow? **A:** `contain: layout` повідомляє браузеру, що зміни всередині елемента не впливають на layout зовні. Це зупиняє поширення reflow вгору по дереву, що важливо для web components та великих компонентних бібліотек. **Q:** Чому `display: none` викликає reflow, а `visibility: hidden` тільки repaint? **A:** `display: none` видаляє елемент з document flow повністю: сусіди зміщуються, батько змінює розміри. `visibility: hidden` зберігає простір, тому нічого навколо не рухається. **Q:** Чому в React StrictMode reflow відбувається двічі? **A:** React StrictMode навмисно запускає ефекти двічі в режимі розробки для виявлення побічних ефектів. У production кожен lifecycle запускається один раз. ## Приклади ### Зміна кольору vs. зміна розміру ```html <!DOCTYPE html> <html> <body> <div id="box" style="width:100px; height:100px; background:red; margin:20px;">Box</div> <div id="sibling" style="background:lightgray; padding:10px;">Сусід</div> <script> const box = document.getElementById('box'); // Тільки repaint: сусід залишається на місці box.style.background = 'blue'; setTimeout(() => { // Reflow: сусід зміщується вниз зі збільшенням висоти box box.style.height = '200px'; }, 1000); </script> </body> </html> ``` Зміна кольору проходить тільки через repaint. Зміна висоти перераховує позиції і `box`, і сусіда. На 100 елементах при 60 кадрах на секунду різниця у часі відмальовування помітна без профайлера. ### Групування читань та записів у grid-інтерфейсі ```js // Контекст: зміна розміру карток у dashboard // Погано: перемежовані читання та записи, один reflow на картку function resizeCardsBad(cards) { cards.forEach(card => { const h = card.offsetHeight; // читання - викликає reflow card.style.height = h + 50 + 'px'; // запис }); } // Правильно: спочатку всі читання, потім всі записи (один reflow) function resizeCardsGood(cards) { const heights = cards.map(card => card.offsetHeight); // всі читання cards.forEach((card, i) => { card.style.height = heights[i] + 50 + 'px'; // всі записи }); } // Ще краще: ResizeObserver прибирає явні layout-читання з коду const observer = new ResizeObserver(entries => { entries.forEach(entry => { const { height } = entry.contentRect; // браузер надає це без примусового reflow entry.target.setAttribute('data-height', height); }); }); cards.forEach(card => observer.observe(card)); ``` Варіант із групуванням зводить N reflow до одного. `ResizeObserver` прибирає явні layout-читання з коду і дозволяє браузеру самому планувати вимірювання. ### Compositor-only анімація (без reflow та repaint) ```js // Погано: left викликає reflow на кожному фреймі function animateBad(element) { let pos = 0; function step() { pos += 2; element.style.left = pos + 'px'; // reflow на кожному фреймі if (pos < 300) requestAnimationFrame(step); } requestAnimationFrame(step); } // Правильно: transform запускається на compositor thread, нуль reflow function animateGood(element) { let pos = 0; element.style.willChange = 'transform'; // переносимо на окремий GPU-шар function step() { pos += 2; element.style.transform = `translateX(${pos}px)`; // тільки compositor if (pos < 300) requestAnimationFrame(step); } requestAnimationFrame(step); } ``` При 60fps поганий варіант запускає 60 reflow на секунду і може блокувати main thread. Хороший варіант запускає нуль. Це зазвичай найбільший виграш у продуктивності анімацій у frontend-проектах.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.