🪿 IT Птица · шпаргалка к собесу

Как работает браузер: рендеринг, Event Loop, анимация

За 30 секунд
ПотокиПуть до кадраPixel pipeline Reflow / thrashingDOMContentLoaded/loadEvent loop Шаг отрисовки🎯 Анимация при забитом стекеСлои ОптимизацияМетрикиВопросы
1

Процессы и потоки

Один процесс на сайт, внутри — несколько потоков. Вся тема — про борьбу за 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.

Процессы (browser / renderer на сайт / GPU / network) нужны для изоляции: краш вкладки не валит браузер.
2

Путь от URL до первого кадра

Critical Rendering Path — последовательность, которую спрашивают почти дословно.

NetworkDNS → TCP/TLS → HTTP, байты HTML стримятся в парсер.
HTML → DOMИнкрементальный разбор. Синхронный <script> блокирует парсер.
CSS → CSSOMRender-blocking: без CSSOM нет отрисовки (иначе FOUC).
Render TreeDOM ⨯ CSSOM, только видимое (display:none выпадает).
Layout (reflow)Геометрия: где и какого размера каждый бокс.
PaintЗаполнение пикселей: текст, цвет, тени, границы.
CompositeСлои → тайлы → финальный кадр с трансформациями.
3

Pixel pipeline: цена изменения

Не каждое изменение проходит весь конвейер. От свойства зависит цена.

JSStyle LayoutPaintComposite
Что меняешьПутьЦена
Геометрия (width, top, margin)Layout → Paint → Composite🔴 дорого (reflow)
Визуал (color, background, тень)Paint → Composite🟡 средне (repaint)
transform, opacityтолько Composite🟢 дёшево
Правило: анимируй transform и opacity, а не left/top/width.
4

Layout thrashing (forced reflow)

Любимая ловушка: чтение геометрии после записи форсит синхронный пересчёт.

❌ Плохо — reflow в цикле
for (const el of items) {
  el.style.height =
    el.offsetHeight + 10 + 'px';
  // read offsetHeight ⇄ write
}
✅ Хорошо — read/write раздельно
const hs = items.map(
  el => el.offsetHeight);   // все чтения
items.forEach((el, i) =>
  el.style.height = hs[i]+10+'px'); // записи
Форсят reflow: offsetHeight, getBoundingClientRect(), scrollTop, getComputedStyle(). Лечение: сначала всё прочитать, потом всё записать.
5

События загрузки

DOMContentLoaded ≠ load. Частый вопрос — кто чего ждёт.

СобытиеКогдаЖдёт ли ресурсы
DOMContentLoadedDOM построенНет — ни картинок, ни CSS, ни фреймов
load (window)всё догруженоДа — картинки, стили, субфреймы

Что сдвигает DOMContentLoaded

• Синхронный <script>задерживает (ждёт скачивания + выполнения).
defer и ES-модули — выполняются прямо перед событием, в порядке объявления → оно их ждёт.
asyncне привязан, выполнится как загрузится.
• CSS перед скриптом — скрипт ждёт CSSOM → косвенно отодвигает событие.

document.readyState: loadinginteractive (= DOMContentLoaded) → complete (= load). На выходе — pagehide/beforeunload; не используй unload — он ломает bfcache (заморозку страницы для мгновенного «назад»).
6

Event loop: порядок выполнения

Один оборот (tick) — и из него растут все задачки «что выведется».

Одна макрозадачавыполняется целиком, пока стек не опустеет
ВСЕ микрозадачидо конца, включая порождённые по ходу — ДО рендера
Рендер — если время кадраrAF → style → layout → paint → composite
↻ и снова с шага 1
Микрозадачи (раньше)Макрозадачи (позже)
Promise.then, queueMicrotask, MutationObserversetTimeout, события, 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)
Render starvation: бесконечные Promise.then не дают дойти до рендера — страница виснет, хотя это не «бесконечный цикл». Микротаски выгребаются ДО отрисовки.
7

Что внутри шага отрисовки

Рендер ≠ только paint. Порядок важен.

scroll/resizerAF Resize/Intersection ObserverStyle→Layout→Paint
rAF срабатывает прямо перед расчётом стилей → изменения попадают в тот же кадр. На фоновой вкладке rAF придушен почти до нуля (Page Visibility API).
8

🎯 Анимация, когда стек забит

Главный вопрос темы. Тяжёлый синхронный JS заблокировал main thread.

❌ Замирает (через main thread)
  • requestAnimationFrame-анимации
  • CSS-анимации left, top, width
  • всё, что требует layout/paint
✅ Играет (через compositor)
  • 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 }.
9

Что выносит элемент на слой

Та самая «магия слоя», на которой держится плавная анимация.

Свой слой получают: <video>, <canvas>, 3D-трансформы, анимируемые transform/opacity, иногда position:fixed. Подсказать заранее — will-change:transform.

Минус: каждый слой — память под текстуру. Слоёв на всё подряд (layer explosion) → выигрыш в одной анимации, проигрыш в памяти. will-change ставь точечно и снимай после.
10

Как держать main thread свободным

Корень тормозов один — занятый поток. Приёмы из него же.

ПриёмЗачем
Дробить long tasks (scheduler.yield, MessageChannel)задача >50 мс блокирует отзывчивость
Web Worker + postMessageтяжёлые вычисления без DOM в своём потоке
requestIdleCallbackнекритичная работа в простое
requestAnimationFrameJS-анимации ровно в кадр (не setTimeout)
contain, content-visibilityограничить область reflow/paint
11

Метрики (Core Web Vitals)

Язык, которым меряют «успел ли браузер».

МетрикаЧто меряетОриентир
FCPпервый контент на экране
LCPсамый крупный элемент отрисован< 2.5 c
CLS«прыжки» вёрстки< 0.1
INP (заменил FID, 2024)скорость реакции на клик< 200 мс
TBTсколько длинные задачи блокировали поток
Все метрики, по сути, меряют одно: как часто и надолго ты отнимал main thread.
12

Реальные вопросы с собесов

Из нашей базы 2000+ собесов — формулировки повторяются.

• Как браузер отрисовывает страницу (compose, layout, reflow)? — PUSK, Сбер, Совкомбанк
• Чем отличаются reflow и repaint? Как оптимизировать? — МТС, ЮТэйр, EnjoyPro
• Event Loop — порядок микро/макрозадач, «что выведется»? — ОрниТех, Restoclub, Иннотех, Ашан Тех
• requestAnimationFrame: как работает, что дешевле всего для анимации (transform)? — ATI.su, IBS, ПСБ
• Что может заблокировать рендеринг? — ИТ Капитал, РСХБ
• Как общаться воркеру с главным потоком? — ФАККТ

Каверзные follow-up

setTimeout(fn, 0) раньше Promise.then?
Нет — микротаски раньше.
Анимация transform тормозит при тяжёлом JS?
Нет, если на своём слое — её ведёт compositor.
Почему страница скроллится, но клики лагают?
Скролл на compositor, обработка событий на main thread.
Бесконечные .then заблокируют отрисовку?
Да — render starvation.
requestIdleCallback гарантированно вызовется?
Нет, но можно задать timeout.