Один неосторожный цикл в JavaScript может сделать сайт медленным и дёрганым. Понимание того, что заставляет браузер пересчитывать геометрию и перерисовывать пиксели, — один из ключевых навыков для создания плавных интерфейсов.
Reflow — это пересчёт геометрии всех элементов страницы. Браузер вычисляет, где должен находиться каждый элемент, какой он ширины и высоты, как он влияет на соседей.
Reflow запускается при любом изменении, которое влияет на геометрию:
width, height, padding, margin, borderoffsetWidth, getBoundingClientRect(), scrollTopReflow — дорогая операция: браузер должен пересчитать всё дерево элементов (или большую его часть), а не только изменившийся элемент.
Repaint — это перерисовка пикселей без изменения геометрии. Меняется только внешний вид: цвет, фон, тень, border-radius, видимость.
Repaint дешевле Reflow, но тоже затратен: нужно перерисовать пиксели элемента и все перекрывающиеся с ним слои.
Операции только Repaint (без Reflow): color, background-color, box-shadow, border-color, outline, visibility.
Самая распространённая ошибка производительности — чтение геометрических свойств сразу после записи стилей в цикле. Браузер вынужден завершить layout до следующего чтения, что приводит к сотням layout-операций за один кадр.
// ПЛОХО: layout thrashing
for (let i = 0; i < items.length; i++) {
items[i].style.width = container.offsetWidth + 'px' // чтение → запись → reflow!
}
// ХОРОШО: сначала читаем, потом пишем
const width = container.offsetWidth // одно чтение
for (let i = 0; i < items.length; i++) {
items[i].style.width = width + 'px' // только запись
}Некоторые CSS-свойства браузер выносит на отдельный GPU-слой и анимирует без участия CPU-рендеринга. Это transform и opacity. GPU-анимации не вызывают ни Reflow, ни Repaint — они работают в 60fps даже на слабых устройствах.
Плохо: анимировать left, top, width, margin — каждый кадр Reflow.
Хорошо: анимировать transform: translateX(), transform: scale(), opacity — только Composite.
Когда нужно добавить много элементов, вставляй их через DocumentFragment. Fragment — это виртуальный контейнер в памяти, который не является частью DOM. При добавлении элементов в Fragment Reflow не происходит. Один Reflow случается только при финальной вставке Fragment в DOM.
requestAnimationFrame(callback) запускает callback перед следующей перерисовкой браузера, обычно ~16.7мс при 60fps. Это правильное место для любых визуальных обновлений: анимации согласуются с ритмом браузера и не вызывают лишних перерисовок.
Layout thrashing vs пакетное обновление — сравнение производительности
// --- МЕДЛЕННЫЙ СПОСОБ: layout thrashing ---
function slowBuildList(container, count) {
const start = performance.now()
for (let i = 0; i < count; i++) {
const item = document.createElement('div')
item.className = 'item'
item.textContent = `Элемент ${i + 1}`
container.appendChild(item) // каждое добавление — Reflow!
// Чтение после записи — принудительный синхронный Reflow
const height = container.scrollHeight // браузер обязан пересчитать
item.style.marginTop = (height > 100) ? '2px' : '4px'
}
console.log(`Медленный способ: ${(performance.now() - start).toFixed(1)} мс`)
}
// --- БЫСТРЫЙ СПОСОБ: DocumentFragment + пакетное обновление ---
function fastBuildList(container, count) {
const start = performance.now()
// Сначала читаем нужные данные
const useSmallMargin = container.scrollHeight > 100
const margin = useSmallMargin ? '2px' : '4px'
// Строим всё в DocumentFragment — без Reflow
const fragment = document.createDocumentFragment()
for (let i = 0; i < count; i++) {
const item = document.createElement('div')
item.className = 'item'
item.textContent = `Элемент ${i + 1}`
item.style.marginTop = margin
fragment.appendChild(item) // нет Reflow — Fragment не в DOM
}
// Один Reflow при финальной вставке
container.appendChild(fragment)
console.log(`Быстрый способ: ${(performance.now() - start).toFixed(1)} мс`)
}
// GPU-анимация через requestAnimationFrame
function animateBox(box) {
let position = 0
function frame() {
position += 2
// transform не вызывает Reflow/Repaint — только Composite
box.style.transform = `translateX(${position}px)`
if (position < 300) requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
}Один неосторожный цикл в JavaScript может сделать сайт медленным и дёрганым. Понимание того, что заставляет браузер пересчитывать геометрию и перерисовывать пиксели, — один из ключевых навыков для создания плавных интерфейсов.
Reflow — это пересчёт геометрии всех элементов страницы. Браузер вычисляет, где должен находиться каждый элемент, какой он ширины и высоты, как он влияет на соседей.
Reflow запускается при любом изменении, которое влияет на геометрию:
width, height, padding, margin, borderoffsetWidth, getBoundingClientRect(), scrollTopReflow — дорогая операция: браузер должен пересчитать всё дерево элементов (или большую его часть), а не только изменившийся элемент.
Repaint — это перерисовка пикселей без изменения геометрии. Меняется только внешний вид: цвет, фон, тень, border-radius, видимость.
Repaint дешевле Reflow, но тоже затратен: нужно перерисовать пиксели элемента и все перекрывающиеся с ним слои.
Операции только Repaint (без Reflow): color, background-color, box-shadow, border-color, outline, visibility.
Самая распространённая ошибка производительности — чтение геометрических свойств сразу после записи стилей в цикле. Браузер вынужден завершить layout до следующего чтения, что приводит к сотням layout-операций за один кадр.
// ПЛОХО: layout thrashing
for (let i = 0; i < items.length; i++) {
items[i].style.width = container.offsetWidth + 'px' // чтение → запись → reflow!
}
// ХОРОШО: сначала читаем, потом пишем
const width = container.offsetWidth // одно чтение
for (let i = 0; i < items.length; i++) {
items[i].style.width = width + 'px' // только запись
}Некоторые CSS-свойства браузер выносит на отдельный GPU-слой и анимирует без участия CPU-рендеринга. Это transform и opacity. GPU-анимации не вызывают ни Reflow, ни Repaint — они работают в 60fps даже на слабых устройствах.
Плохо: анимировать left, top, width, margin — каждый кадр Reflow.
Хорошо: анимировать transform: translateX(), transform: scale(), opacity — только Composite.
Когда нужно добавить много элементов, вставляй их через DocumentFragment. Fragment — это виртуальный контейнер в памяти, который не является частью DOM. При добавлении элементов в Fragment Reflow не происходит. Один Reflow случается только при финальной вставке Fragment в DOM.
requestAnimationFrame(callback) запускает callback перед следующей перерисовкой браузера, обычно ~16.7мс при 60fps. Это правильное место для любых визуальных обновлений: анимации согласуются с ритмом браузера и не вызывают лишних перерисовок.
Layout thrashing vs пакетное обновление — сравнение производительности
// --- МЕДЛЕННЫЙ СПОСОБ: layout thrashing ---
function slowBuildList(container, count) {
const start = performance.now()
for (let i = 0; i < count; i++) {
const item = document.createElement('div')
item.className = 'item'
item.textContent = `Элемент ${i + 1}`
container.appendChild(item) // каждое добавление — Reflow!
// Чтение после записи — принудительный синхронный Reflow
const height = container.scrollHeight // браузер обязан пересчитать
item.style.marginTop = (height > 100) ? '2px' : '4px'
}
console.log(`Медленный способ: ${(performance.now() - start).toFixed(1)} мс`)
}
// --- БЫСТРЫЙ СПОСОБ: DocumentFragment + пакетное обновление ---
function fastBuildList(container, count) {
const start = performance.now()
// Сначала читаем нужные данные
const useSmallMargin = container.scrollHeight > 100
const margin = useSmallMargin ? '2px' : '4px'
// Строим всё в DocumentFragment — без Reflow
const fragment = document.createDocumentFragment()
for (let i = 0; i < count; i++) {
const item = document.createElement('div')
item.className = 'item'
item.textContent = `Элемент ${i + 1}`
item.style.marginTop = margin
fragment.appendChild(item) // нет Reflow — Fragment не в DOM
}
// Один Reflow при финальной вставке
container.appendChild(fragment)
console.log(`Быстрый способ: ${(performance.now() - start).toFixed(1)} мс`)
}
// GPU-анимация через requestAnimationFrame
function animateBox(box) {
let position = 0
function frame() {
position += 2
// transform не вызывает Reflow/Repaint — только Composite
box.style.transform = `translateX(${position}px)`
if (position < 300) requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
}Оптимизируй функцию buildList: вместо прямой вставки в DOM используй DocumentFragment. Сравни время выполнения медленной и быстрой версии с помощью performance.now(). Добавь в контейнер 500 элементов.
Создай fragment через document.createDocumentFragment(). Добавляй div-элементы в fragment (не в container). После цикла вставь fragment в container одной операцией appendChild.