Reflow и Repaint: защо малки промени в DOM могат да забавят страницата

Reflow се случва, когато страницата на DOM променя своето оформление. Repaint се случва, когато има промени само в стила на външния вид, като цвят и видимост.

Ако вече знаеш как работи Critical Rendering Path, знаеш и че браузърът минава през Layout, Paint и Composite при първоначалното зареждане на страницата. Но какво се случва, когато JavaScript промени нещо след зареждането – добави елемент, промени размер, смени цвят? Браузърът трябва да повтори част от тези стъпки. Тези повторения се казват Reflow и Repaint, и те са един от най-честите скрити причини за бавни и „дърдорещи“ интерфейси (chatty interfaces).

Reflow и Repaint: защо малки промени в DOM могат да забавят страницата
Reflow и Repaint: защо малки промени в DOM могат да забавят страницата

Какво е Reflow?

Reflow (известен също като Layout) е процесът, при който браузърът преизчислява геометрията на елементите: техните позиции, размери и взаимоотношения в страницата.

Reflow се задейства при всяка промяна, която засяга пространственото оформление:

  • Промяна на ширина, височина, padding, margin, border;
  • Добавяне или премахване на DOM елементи;
  • Промяна на шрифт или размер на текст;
  • Промяна на display стойност;
  • Преоразмеряване на прозореца (window resize);
  • Четене на определени DOM свойства (за това – по-надолу).

Защо Reflow е скъпа операция?

Защото е каскадна. Когато един елемент се промени, браузърът трябва да провери дали промяната му влияе на съседните елементи, на родителя, на децата. При сложна страница с много вложени елементи, един Reflow може да засегне стотици елементи едновременно.

Промяна на ширината на div
  → Децата му се преизчисляват
    → Съседните им елементи се преизчисляват
      → Родителят се преизчислява
        → ...

Какво е Repaint?

Repaint е процесът, при който браузърът пренарисува пикселите на елементите – цветове, фонове, сенки, граници, текст – без да променя геометрията им.

Repaint се задейства при промени, засягащи само визуалния вид:

  • Промяна на color, background-color, border-color;
  • Промяна на box-shadow, outline;
  • Промяна на visibility (но не display);
  • Промяна на border-radius (само визуален ефект).

Reflow и Repaint: Каква е разликата между тях?

ReflowRepaint
Друго имеLayoutPaint
ЗасягаГеометрия (позиция, размер)Визуален вид (цвят, фон)
Задейства Repaint?ВинагиНе задейства Reflow
Скъпа ли е?Много скъпаПо-малко скъпа от Reflow
Засяга ли съседи?Да, каскадноНе

Ключовото правило: всеки Reflow задейства Repaint, но не всеки Repaint задейства Reflow.

Как Reflow и Repaint се вписват в Critical Rendering Path (CRP)

В предишната статия видяхме, че CRP е:

DOM → CSSOM → Render Tree → Layout → Paint → Composite

Reflow и Repaint са просто повторното изпълнение на Layout и Paint стъпките след първоначалното зареждане. Разликата е, че вместо да се изпълняват веднъж при зареждане, те могат да се изпълнят десетки пъти в секунда при активна JavaScript манипулация.

При оптимизирани промени (само transform и opacity) браузърът прескача Layout и Paint изцяло и работи само на Composite ниво — затова те са толкова препоръчвани за анимации.

Промяна на width/height   → Layout → Paint → Composite  (скъпо)
Промяна на color          →          Paint → Composite  (средно)
Промяна на transform      →                  Composite  (евтино)

Какво задейства Reflow в реален код?

Директни причинители

// Промяна на размер
element.style.width = '500px';
element.style.padding = '20px';
element.style.fontSize = '18px';

// DOM манипулация
document.body.appendChild(newElement);
element.remove();

// Класове, засягащи геометрия
element.classList.add('expanded'); // ако .expanded има width/height

Скритият убиец: Forced Synchronous Layout

Това е най-коварната причина за Reflow. Повечето разработчици не я осъзнават.

Браузърът е умен. Когато правиш много промени наведнъж, той ги натрупва и изпълнява Layout веднъж накрая (batching). Но ако прочетеш геометрично свойство след като си написал нещо, браузърът е принуден да изпълни Layout незабавно, защото трябва да ти върне актуална стойност.

// ❌ Forced Synchronous Layout — Reflow при всяка итерация
for (let i = 0; i < elements.length; i++) {
  elements[i].style.width = '100px';          // Запис → маркира Layout като "dirty"
  const height = elements[i].offsetHeight;    // Четене → принуждава незабавен Layout
}
// Ако имаш 100 елемента → 100 Reflow-а
// ✅ Правилно — прочети всичко първо, после пиши
const heights = elements.map(el => el.offsetHeight);  // Всички четения наведнъж

for (let i = 0; i < elements.length; i++) {
  elements[i].style.width = heights[i] + 'px';  // Всички записи наведнъж
}
// 1 Reflow общо

DOM свойства, които задействат Reflow при четене

Следните свойства форсират незабавен Layout когато ги четеш:

СвойствоВид
offsetWidth, offsetHeightРазмери с border
clientWidth, clientHeightРазмери без border
scrollWidth, scrollHeightScroll размери
offsetTop, offsetLeftПозиция спрямо родител
getBoundingClientRect()Пълна геометрия
scrollTop, scrollLeftScroll позиция
getComputedStyle()Изчислени стилове

Layout Thrashing

Layout Thrashing е термин за ситуацията, в която Forced Synchronous Layout се случва многократно в бърза последователност, обикновено в цикъл.

Резултатът е видим с просто око: интерфейсът не е плавен, скролът е неравен, анимациите пропускат кадри (janky interface).

// ❌ Класически Layout Thrashing
function resizeAllBoxes() {
  boxes.forEach(box => {
    const containerWidth = container.offsetWidth; // Четене → форсира Layout
    box.style.width = containerWidth / 2 + 'px';  // Запис → маркира като dirty
    // При следваща итерация → четенето отново форсира Layout
  });
}
// ✅ Решение: разделяне на четене и запис
function resizeAllBoxes() {
  const containerWidth = container.offsetWidth; // Едно четене

  boxes.forEach(box => {
    box.style.width = containerWidth / 2 + 'px'; // Само записи
  });
}

FastDOM библиотека

За по-сложни случаи съществува библиотеката FastDOM, която автоматично разделя четенията и записите:

import fastdom from 'fastdom';

fastdom.measure(() => {
  // Всички четения тук
  const width = element.offsetWidth;

  fastdom.mutate(() => {
    // Всички записи тук
    element.style.width = width + 'px';
  });
});

Как да намалиш Reflow и Repaint

1. Използвай CSS класове вместо inline стилове

// ❌ Три отделни Reflow-а
element.style.width = '200px';
element.style.height = '100px';
element.style.margin = '10px';

// ✅ Един Reflow
element.classList.add('card--expanded');
.card--expanded {
  width: 200px;
  height: 100px;
  margin: 10px;
}

2. Използвай transform и opacity за анимации

Тези две CSS свойства се обработват само от GPU на Composite ниво — без Layout, без Paint.

/* ❌ Задейства Layout при всяка стъпка на анимацията */
@keyframes move-bad {
  from { left: 0; top: 0; }
  to   { left: 200px; top: 100px; }
}

/* ✅ Само Composite — GPU анимация, без Layout/Paint */
@keyframes move-good {
  from { transform: translate(0, 0); }
  to   { transform: translate(200px, 100px); }
}

3. Използвай will-change за предстоящи анимации

.animated-card {
  will-change: transform; /* Казва на браузъра да създаде отделен compositing layer */
}

Внимание: Не прилагай will-change на всичко – всеки compositing layer заема памет. Използвай го само за елементи, които реално ще се анимират.

4. Batch DOM промени с DocumentFragment

Когато добавяш много елементи, не ги добавяй един по един в живия DOM:

// ❌ 100 Reflow-а
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `Елемент ${i}`;
  list.appendChild(li); // Всяко appendChild задейства Reflow
}

// ✅ 1 Reflow
const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `Елемент ${i}`;
  fragment.appendChild(li); // Работи извън живия DOM
}

list.appendChild(fragment); // Един Reflow в края

5. Скрий елемента преди масови промени

// ✅ Скрий → промени → покажи
element.style.display = 'none'; // 1 Reflow

// Направи 100 промени — те не задействат Reflow (елементът е извън Render Tree)
element.style.width = '200px';
element.style.height = '100px';
// ... още промени ...

element.style.display = 'block'; // 1 Reflow
// Общо: 2 Reflow вместо 100

6. Ограничи дълбочината на CSS селекторите

По-дълбоките CSS селектори означават, че браузърът обхожда повече от DOM дървото при всеки Reflow:

/* ❌ Дълбок селектор — по-бавен Reflow */
.page-wrapper .content-area .article-list .article-item .title span { }

/* ✅ Плосък селектор — по-бърз Reflow */
.article-title { }

А ето и официалната документация на Google за Minimizing browser reflow, която обяснява reflow като user-blocking операция и дава конкретни препоръки за намаляването му: Minimizing browser reflow.

Как да измериш Reflow и Repaint

Chrome DevTools – Performance таб

Иди на

1. DevTools–>таб Performance.

2. Кликни на иконата ⚙️ (настройки) горе вдясно в панела.

3. Включи „Enable advanced paint instrumentation“

4. Запиши с 🔄 бутона (Record and reload)

Как да измериш Reflow и Repaint
Как да измериш Reflow и Repaint

5. В резултата търси:

  • Лилави блокове – Layout (Reflow).
  • Зелени блокове – Paint (Repaint).
  • Ако са много и чести, то имаш проблем.
Лилави блокове - Layout (Reflow) и зелени блокове - Paint (Repaint)
Лилави блокове – Layout (Reflow) и зелени блокове – Paint (Repaint).

    Rendering таб (за визуална диагностика)

    1. DevTools–>меню с три точки (⋮) горе вдясно.
    2. More tools–>Rendering.
    3. Включи „Paint flashing“ – страницата ще мига в зелено при всеки Repaint в реално време.

    Скролни страницата или задействай интеракции. Ако цялата страница мига зелено при скрол – имаш проблем с ненужни Repaint-и.

    Обяснявам подрообно: В Rendering таба има много опции и Paint Flashing не е първата. Търси го така:

    1. След като отвориш Rendering таба (появява се като отделен панел най-долу в DevTools).
    2. Скролни надолу в него – опциите са наредени вертикално.
    3. Търси чекбокс с надпис „Paint flashing“ – той е приблизително по средата на списъка, след опции като „Disable cache“ и „Emulate CSS media“.

    Изглежда така (сложи отметка):

    Rendering таб (за визуална диагностика)
    Rendering таб (за визуална диагностика)

    Сега превключи към самата страница (не DevTools) и скролни или задействай нещо – зелените мигания ще се появят върху страницата

    Конкретни числа: колко е „твърде много“?

    Браузърът цели 60 кадъра в секунда — това означава 16.67ms на кадър. Ако Layout + Paint + JavaScript надхвърлят 16ms, кадърът се пропуска и потребителят вижда „дърдорене“.

    Reflow durationОценка
    < 1msОтлично
    1–5msДобро
    5–10msПриемливо
    > 10msПроблематично при 60fps

    Изчерпателен списък на свойствата, задействащи Reflow „What forces layout/reflow“, изграден чрез четене на Blink source кода. Валиден е за Chrome, Opera, Brave, Edge и повечето Android браузъри. Paul Irish е от екипа на Chrome DevTools и този документ е стандартна референция, цитирана навсякъде в performance общността.

    Containment: CSS isolation за Reflow

    contain е сравнително нов CSS property, който казва на браузъра: „промените вътре в този елемент не засягат нищо извън него“.

    .widget {
      contain: layout; /* Reflow вътре не се разпространява навън */
    }
    
    .card {
      contain: strict; /* Пълна изолация — layout + paint + size */
    }
    

    Това е изключително полезно за компоненти като чат съобщения, карти с продукти или всякакви повтарящи се UI елементи. Браузърът може да преизчисли само засегнатия компонент, без да засяга останалата страница.

    content-visibility: auto

    content-visibility: auto е мощна оптимизация, при която браузърът пропуска Layout и Paint за елементи извън видимата зона изцяло:

    .article-section {
      content-visibility: auto;
      contain-intrinsic-size: 0 500px; /* Placeholder размер за scrollbar */
    }
    

    При дълги страници (блог пост, продуктов каталог) това може да намали времето за рендиране с 40-60%. Браузърът рендира само видимото, а не цялата страница наведнъж.

    Reflow, Repaint и JavaScript Frameworks

    React и Virtual DOM

    React използва Virtual DOM именно за да минимизира Reflow и Repaint. Вместо да записва директно в реалния DOM (и задейства Layout при всяка промяна), React:

    1. Прилага промените във виртуален DOM (в памет – без Layout)
    2. Сравнява новото с предишното състояние (diffing)
    3. Записва в реалния DOM само разликите в един batch запис

    Резултатът е минимален брой Reflow-и, независимо от броя на state промените.

    Vue и реактивността

    Vue работи по подобен начин – чрез реактивна система, която групира DOM промените и ги прилага в следващия „tick“ (асинхронно), вместо синхронно след всяка промяна.

    Честите грешки, причиняващи излишен Reflow

    Анимиране на width/height с CSS transitions

    /* ❌ Задейства Layout при всяка стъпка */
    .card {
      transition: width 0.3s, height 0.3s;
    }
    
    /* ✅ Само Composite */
    .card {
      transition: transform 0.3s;
    }
    .card--expanded {
      transform: scale(1.2);
    }
    

    Четене на offsetWidth в scroll event handler

    // ❌ Форсира Reflow при всеки scroll event (60 пъти/сек)
    window.addEventListener('scroll', () => {
      const width = element.offsetWidth; // Форсира Layout
      doSomethingWith(width);
    });
    
    // ✅ Кешира стойността
    let cachedWidth = element.offsetWidth;
    
    window.addEventListener('resize', () => {
      cachedWidth = element.offsetWidth; // Обновява само при resize
    });
    
    window.addEventListener('scroll', () => {
      doSomethingWith(cachedWidth); // Без Layout
    });
    

    Позициониране с top/left вместо transform

    // ❌ Задейства Layout при всяка стъпка на анимацията
    function animate(timestamp) {
      element.style.left = position + 'px'; // Layout → Paint → Composite
      requestAnimationFrame(animate);
    }
    
    // ✅ Само Composite
    function animate(timestamp) {
      element.style.transform = `translateX(${position}px)`; // Composite only
      requestAnimationFrame(animate);
    }
    

    Как Reflow/Repaint се свързва с Event Loop

    Reflow и Repaint не се случват произволно. Браузърът ги планира в определен момент от Event Loop цикъла. Конкретно, те се изпълняват в специална фаза, наречена rendering steps, която се случва след JavaScript задачите и микрозадачите.

    Това е причината, поради която браузърът може да „батчва“ множество DOM промени. Той изчаква JavaScript да завърши, и едва тогава изпълнява Layout и Paint веднъж.

    Event Loop цикъл:
    [JavaScript Task] → [Microtasks] → [Rendering: Style → Layout → Paint → Composite]
    

    Следващата статия от поредицата – Event Loop – разглежда точно тази механика: как Tasks, Microtasks и rendering steps се нареждат и изпълняват, и защо това е от ключово значение за отзивчивостта на интерфейса.

    Обобщение

    Reflow е преизчисляване на геометрията и е скъпа, каскадна операция. Repaint е пренарисуване на пикселите и е по-евтина, но все пак значима.

    Ключови изводи:

    • Всеки Reflow задейства Repaint, но не обратното.
    • Forced Synchronous Layout (четене след запис в цикъл) е най-честата причина за Layout Thrashing.
    • Анимирай само с transform и opacity – те минават директно на Composite ниво.
    • Групирай DOM промените – прочети всичко, после пиши всичко.
    • DocumentFragment, contain и content-visibility са мощни инструменти за сложни случаи.
    • Chrome DevTools Paint Flashing визуализира Repaint проблемите в реално време.
    • 60fps = 16.67ms на кадър – това е бюджетът за целия Layout + Paint + JavaScript.

    Всички тези операции се изпълняват в основната нишка на браузъра.
    За да разберем как JavaScript се изпълнява заедно с rendering процеса, трябва да разгледаме Event Loop.

    Другите статии от серията:

    Ако ви е харесала публикацията, споделете я:

    Оставете коментар

    Вашият имейл адрес няма да бъде публикуван. Задължителните полета са отбелязани с *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.