- Браузер превращает текст в пиксели ~60 раз/сек → бюджет 16.7 мс на кадр.
- Почти всё — на main thread (там же твой JS). Занял его → встало всё.
- Конвейер кадра: JS → Style → Layout → Paint → Composite.
transform/opacity= только Composite, его крутит compositor (не main thread) → дёшево.- Event loop: 1 макротаска → ВСЕ микротаски → (если время) рендер.
- Стек забит → JS-анимации замирают, но transform/opacity на своём слое играют (их ведёт compositor).
Процессы и потоки
Один процесс на сайт, внутри — несколько потоков. Вся тема — про борьбу за main thread.
Main thread
DOM, CSSOM, стили, layout, paint, JS и event loop. Узкое место — занял, встало всё.
Compositor thread + GPU
Складывает готовые слои в кадр, ведёт скролл и transform/opacity-анимации. Не зависит от main thread.
Raster threads
Превращают команды рисования в пиксели (тайлы).
Web Workers
JS в отдельном потоке, но без доступа к DOM. Связь через postMessage.
Путь от URL до первого кадра
Critical Rendering Path — последовательность, которую спрашивают почти дословно.
<script> блокирует парсер.display:none выпадает).Pixel pipeline: цена изменения
Не каждое изменение проходит весь конвейер. От свойства зависит цена.
| Что меняешь | Путь | Цена |
|---|---|---|
Геометрия (width, top, margin) | Layout → Paint → Composite | 🔴 дорого (reflow) |
Визуал (color, background, тень) | Paint → Composite | 🟡 средне (repaint) |
transform, opacity | только Composite | 🟢 дёшево |
transform и opacity, а не left/top/width.Layout thrashing (forced reflow)
Любимая ловушка: чтение геометрии после записи форсит синхронный пересчёт.
for (const el of items) {
el.style.height =
el.offsetHeight + 10 + 'px';
// read offsetHeight ⇄ write
}const hs = items.map(
el => el.offsetHeight); // все чтения
items.forEach((el, i) =>
el.style.height = hs[i]+10+'px'); // записиoffsetHeight, getBoundingClientRect(), scrollTop, getComputedStyle(). Лечение: сначала всё прочитать, потом всё записать.События загрузки
DOMContentLoaded ≠ load. Частый вопрос — кто чего ждёт.
| Событие | Когда | Ждёт ли ресурсы |
|---|---|---|
DOMContentLoaded | DOM построен | Нет — ни картинок, ни CSS, ни фреймов |
load (window) | всё догружено | Да — картинки, стили, субфреймы |
Что сдвигает DOMContentLoaded
• Синхронный <script> — задерживает (ждёт скачивания + выполнения).
• defer и ES-модули — выполняются прямо перед событием, в порядке объявления → оно их ждёт.
• async — не привязан, выполнится как загрузится.
• CSS перед скриптом — скрипт ждёт CSSOM → косвенно отодвигает событие.
document.readyState: loading → interactive (= DOMContentLoaded) → complete (= load). На выходе — pagehide/beforeunload; не используй unload — он ломает bfcache (заморозку страницы для мгновенного «назад»).Event loop: порядок выполнения
Один оборот (tick) — и из него растут все задачки «что выведется».
| Микрозадачи (раньше) | Макрозадачи (позже) |
|---|---|
Promise.then, queueMicrotask, MutationObserver | setTimeout, события, I/O, MessageChannel |
console.log('1');
setTimeout(() => console.log('2')); // макро
Promise.resolve().then(() => console.log('3')); // микро
console.log('4');
// → 1, 4, 3, 2 (микро 3 раньше макро 2)
Promise.then не дают дойти до рендера — страница виснет, хотя это не «бесконечный цикл». Микротаски выгребаются ДО отрисовки.Что внутри шага отрисовки
Рендер ≠ только paint. Порядок важен.
🎯 Анимация, когда стек забит
Главный вопрос темы. Тяжёлый синхронный JS заблокировал main thread.
requestAnimationFrame-анимации- CSS-анимации
left,top,width - всё, что требует layout/paint
transform,opacityна своём слое- скролл страницы
- работает на GPU, не ждёт main thread
Почему transform/opacity выживают
Картинка элемента уже нарисована в отдельный слой. Для следующего кадра не нужны ни layout, ни paint — только сдвинуть/затемнить готовый слой. Это делает compositor + GPU, который не зависит от заблокированного main thread.
/* Переживёт тяжёлый JS — на compositor */
.spinner{ animation:spin 1s linear infinite; will-change:transform; }
@keyframes spin{ to{ transform:rotate(360deg); } }
/* Замрёт — left идёт через layout на main thread */
.bad{ animation:move 1s linear infinite; }
@keyframes move{ to{ left:300px; } }
wheel/touchmove висит не-passive обработчик (браузер ждёт возможный preventDefault). → { passive:true }.Что выносит элемент на слой
Та самая «магия слоя», на которой держится плавная анимация.
Свой слой получают: <video>, <canvas>, 3D-трансформы, анимируемые transform/opacity, иногда position:fixed. Подсказать заранее — will-change:transform.
will-change ставь точечно и снимай после.Как держать main thread свободным
Корень тормозов один — занятый поток. Приёмы из него же.
| Приём | Зачем |
|---|---|
Дробить long tasks (scheduler.yield, MessageChannel) | задача >50 мс блокирует отзывчивость |
Web Worker + postMessage | тяжёлые вычисления без DOM в своём потоке |
requestIdleCallback | некритичная работа в простое |
requestAnimationFrame | JS-анимации ровно в кадр (не setTimeout) |
contain, content-visibility | ограничить область reflow/paint |
Метрики (Core Web Vitals)
Язык, которым меряют «успел ли браузер».
| Метрика | Что меряет | Ориентир |
|---|---|---|
| FCP | первый контент на экране | — |
| LCP | самый крупный элемент отрисован | < 2.5 c |
| CLS | «прыжки» вёрстки | < 0.1 |
| INP (заменил FID, 2024) | скорость реакции на клик | < 200 мс |
| TBT | сколько длинные задачи блокировали поток | — |
Реальные вопросы с собесов
Из нашей базы 2000+ собесов — формулировки повторяются.
• Как браузер отрисовывает страницу (compose, layout, reflow)? — PUSK, Сбер, Совкомбанк
• Чем отличаются reflow и repaint? Как оптимизировать? — МТС, ЮТэйр, EnjoyPro
• Event Loop — порядок микро/макрозадач, «что выведется»? — ОрниТех, Restoclub, Иннотех, Ашан Тех
• requestAnimationFrame: как работает, что дешевле всего для анимации (transform)? — ATI.su, IBS, ПСБ
• Что может заблокировать рендеринг? — ИТ Капитал, РСХБ
• Как общаться воркеру с главным потоком? — ФАККТ