Блог за уеб технологии, маркетинг и SEO, мотивация и продуктивност
Reflow и Repaint: защо малки промени в DOM могат да забавят страницата
Reflow се случва, когато страницата на DOM променя своето оформление. Repaint се случва, когато има промени само в стила на външния вид, като цвят и видимост.
Ако вече знаеш как работи Critical Rendering Path, знаеш и че браузърът минава през Layout, Paint и Composite при първоначалното зареждане на страницата. Но какво се случва, когато JavaScript промени нещо след зареждането – добави елемент, промени размер, смени цвят? Браузърът трябва да повтори част от тези стъпки. Тези повторения се казват Reflow и Repaint, и те са един от най-честите скрити причини за бавни и „дърдорещи“ интерфейси (chatty interfaces).
Съдържание на тази страница:
Какво е 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: Каква е разликата между тях?
| Reflow | Repaint | |
|---|---|---|
| Друго име | Layout | Paint |
| Засяга | Геометрия (позиция, размер) | Визуален вид (цвят, фон) |
| Задейства 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, scrollHeight | Scroll размери |
offsetTop, offsetLeft | Позиция спрямо родител |
getBoundingClientRect() | Пълна геометрия |
scrollTop, scrollLeft | Scroll позиция |
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)

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

Rendering таб (за визуална диагностика)
- DevTools–>меню с три точки (⋮) горе вдясно.
- More tools–>Rendering.
- Включи „Paint flashing“ – страницата ще мига в зелено при всеки Repaint в реално време.
Скролни страницата или задействай интеракции. Ако цялата страница мига зелено при скрол – имаш проблем с ненужни Repaint-и.
Обяснявам подрообно: В Rendering таба има много опции и Paint Flashing не е първата. Търси го така:
- След като отвориш Rendering таба (появява се като отделен панел най-долу в DevTools).
- Скролни надолу в него – опциите са наредени вертикално.
- Търси чекбокс с надпис „Paint flashing“ – той е приблизително по средата на списъка, след опции като „Disable cache“ и „Emulate CSS media“.
Изглежда така (сложи отметка):

Сега превключи към самата страница (не 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:
- Прилага промените във виртуален DOM (в памет – без Layout)
- Сравнява новото с предишното състояние (diffing)
- Записва в реалния 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.
Другите статии от серията:
- Какво е API? Просто обяснение и архитектурен контекст
- Какво е DOM и защо без него уеб не би бил интерактивен
- Как браузърите парсват HTML (и какво означава това за SEO)
- Какво е DNS и как намира сайтовете в интернет
- Какво е CDN и защо ускорява сайтовете
- Как работи HTTP: протоколът зад всяка уеб страница
- Critical Rendering Path: процесът, който определя скоростта на сайта



