Frontend Interview: браузерный пайплайн, Promises, генераторы и производительность рендеринга

Курс системно разбирает путь от ввода URL до завершения рендеринга, а также ключевые механизмы JavaScript (event loop, промисы, генераторы). Отдельный фокус — рендеринг в браузере: reflow/layout, repaint, compositing и практические триггеры в JS/CSS, влияющие на производительность.

1. От URL до первого байта: DNS, TCP/TLS, HTTP, кеши и Service Worker

!Иллюстрация

Подготовка к собеседованию в Яндекс: Browser Pipeline, Promises и Генераторы

На технических интервью в крупные компании (как Яндекс, Avito, Тинькофф) требуют глубокого понимания того, как работает платформа. Вас спросят не только о том, как написать код, но и о том, какой ценой он выполняется. В этой статье мы разберем полный путь работы браузера, механизмы рендеринга и внутреннее устройство асинхронности в JavaScript.

!Сделай мне превью для публикации в Telegram канал, про gurufy. Gurufy - это сервис который позволяет создавать курсы с помощью ИИ

Часть 1. Browser Pipeline: От URL до пикселей

Этот процесс делится на два больших этапа: сетевой (доставка ресурсов) и рендеринг (превращение кода в картинку).

!Временная шкала от ввода URL до получения первого байта

1. Сетевой этап (Network)

Когда пользователь вводит URL и нажимает Enter, браузер выполняет следующие шаги:

  • Разбор URL и проверка кешей. Браузер проверяет HSTS (нужен ли HTTPS), Service Worker и HTTP-кеш (Disk/Memory). Если ресурс свежий, сеть не используется.
  • DNS (Domain Name System). Если кеша нет, браузер должен узнать IP-адрес сервера. Запрос идет от локального кеша к DNS-серверу провайдера и далее рекурсивно.
  • Установка соединения (TCP/TLS).
  • * TCP Handshake: Обмен пакетами SYNSYN-ACKACK для создания надежного канала. * TLS Handshake: Обмен сертификатами и ключами шифрования. В TLS 1.3 это происходит быстрее (1 RTT), но это все равно затратная операция.
  • HTTP-запрос и ответ. Отправляется запрос (GET). Сервер думает (Time To First Byte — TTFB) и начинает отдавать HTML.
  • 2. Построение объектных моделей (Parsing)

    Как только браузер получает первые байты HTML, он начинает парсинг, не дожидаясь загрузки всего документа.

  • HTML → DOM (Document Object Model).
  • * Байты → Символы → Токены (теги) → Узлы (Nodes) → DOM-дерево. * Если парсер встречает <script>, он блокирует построение DOM (если нет атрибутов async или defer), загружает и выполняет JS.
  • CSS → CSSOM (CSS Object Model).
  • * Браузер загружает стили и строит дерево правил. CSSOM блокирует рендеринг: браузер не покажет контент, пока не поймет, как его стилизовать.

    !Создай фото правил в веб разработке

    3. Render Tree

    Браузер объединяет DOM и CSSOM в Render Tree (дерево рендеринга). В него попадают только видимые элементы.

    * Элементы с display: none не попадают в Render Tree. * Элементы с visibility: hidden попадают (они занимают место, просто прозрачные). * Псевдоэлементы (::before, ::after) добавляются сюда.

    4. Layout (Reflow) — Компоновка

    На этом этапе браузер рассчитывает геометрию каждого элемента: точные координаты () и размеры () в пикселях экрана. Процесс идет рекурсивно от <html> вглубь.

    Понятие Reflow (перекомпоновка) означает повторный запуск этого процесса при изменениях.

    5. Paint (Repaint) — Отрисовка

    Браузер заполняет пиксели: цвета, фоны, тени, границы, текст. Отрисовка часто происходит на нескольких слоях.

    Понятие Repaint (перерисовка) означает обновление внешнего вида без изменения геометрии.

    6. Composite — Композиция

    Финальный этап. Браузер собирает все слои в итоговое изображение на экране. Этот этап выполняется на GPU (видеокарте), что делает его очень быстрым.

    Часть 2. Reflow, Repaint и Composite: Детальный разбор

    Понимание этих терминов критично для оптимизации производительности (60 FPS).

    Reflow (Layout)

    Самая дорогая операция. Происходит, когда меняется геометрия страницы или структура дерева.

    Что вызывает Reflow: * Добавление/удаление DOM-узлов. * Изменение размеров окна браузера (resize). * Изменение шрифта. * Изменение свойств: width, height, padding, margin, border, left, top. * Чтение геометрических свойств (Forced Synchronous Layout). Если вы меняете стиль, а потом сразу читаете offsetWidth, браузер обязан прервать JS и срочно пересчитать макет, чтобы дать актуальные данные.

    Repaint

    Средняя по стоимости операция. Происходит, когда меняется внешний вид, но не геометрия.

    Что вызывает Repaint: * color, background-color. * visibility. * box-shadow, outline.

    Composite

    Самая дешевая операция. Если свойство можно обработать только на этапе композиции, Layout и Paint пропускаются.

    Свойства для Composite (GPU): * transform (перемещение, масштабирование, вращение). * opacity (прозрачность). * filter (некоторые фильтры).

    > Для плавных анимаций всегда используйте transform и opacity. Изменение left/top вызывает Reflow каждый кадр, что нагружает CPU.

    Часть 3. Promises (Промисы)

    Промис — это объект, представляющий результат асинхронной операции, который будет получен в будущем. Это замена callback-hell.

    Внутреннее устройство

    У промиса есть три состояния:

  • Pending (ожидание) — исходное состояние.
  • Fulfilled (исполнено) — операция завершена успешно, есть результат (value).
  • Rejected (отклонено) — произошла ошибка, есть причина (reason).
  • Смена состояния необратима. Из Fulfilled нельзя перейти в Rejected и наоборот.

    Microtask Queue

    Обработчики .then(), .catch(), .finally() выполняются асинхронно через Microtask Queue. Микрозадачи имеют приоритет перед макрозадачами (setTimeout, I/O) и выполняются сразу после текущего синхронного кода, но до рендеринга.

    Пример порядка выполнения:

    Ключевые методы

    * Promise.all([p1, p2]) — ждет выполнения всех. Если хоть один упал — падает весь all. * Promise.allSettled([p1, p2]) — ждет завершения всех (неважно, успех или ошибка). Возвращает массив статусов. * Promise.race([p1, p2]) — возвращает результат первого завершившегося (успех или ошибка). * Promise.any([p1, p2]) — возвращает первый успешный. Ошибка будет только если все упали.

    Часть 4. Генераторы (Generators)

    Генераторы — это функции, выполнение которых можно приостанавливать и возобновлять. Они лежат в основе async/await и библиотек вроде Redux-Saga.

    Синтаксис и работа

    Обозначаются звездочкой function*. При вызове код функции не выполняется сразу, а возвращается объект итератора.

    Ключевое слово yield

    yield делает две вещи:

  • Возвращает значение наружу (как return).
  • Ставит выполнение функции на паузу, сохраняя локальные переменные.
  • Двусторонний обмен данными

    В генератор можно передавать данные через next(value). Значение попадет в переменную слева от yield, на котором стояла пауза.

    Это позволяет писать асинхронный код в синхронном стиле, что и реализовано в синтаксическом сахаре async/await (где await — это автоматический yield промиса).

    Итоги

  • Пайплайн браузера: URL → DNS → TCP/TLS → HTTP → Parsing (DOM/CSSOM) → Render Tree → Layout → Paint → Composite.
  • Оптимизация рендеринга: Избегайте Reflow (изменения геометрии). Для анимаций используйте transform и opacity, так как они вызывают только Composite и обрабатываются на GPU.
  • Промисы: Управляют асинхронностью через Microtask Queue. Важно знать методы all, race, allSettled, any.
  • Генераторы: Функции с возможностью паузы (yield). Позволяют создавать итераторы и управлять потоком выполнения, являясь фундаментом для async/await.
  • 2. Парсинг и выполнение: HTML/CSS/JS, DOM/CSSOM, загрузка ресурсов

    Парсинг и выполнение: HTML/CSS/JS, DOM/CSSOM, загрузка ресурсов

    Как эта часть пайплайна связана с предыдущей статьёй

    В прошлой статье мы дошли до TTFB: браузер получил первый байт HTML, пройдя URL → кеши/Service Worker → DNS → TCP/TLS → HTTP.

    Дальше начинается критический путь рендеринга: браузер должен превратить поток байтов HTML/CSS/JS в структуры данных и понять, что и когда можно рисовать.

    Эта статья отвечает на вопросы:

  • Как HTML превращается в DOM
  • Как CSS превращается в CSSOM
  • Почему CSS и некоторые скрипты блокируют рендер
  • Как браузер находит и приоритизирует загрузку ресурсов
  • Что означают DOMContentLoaded и load
  • > Рекомендуемая база от платформы: How browsers work

    !Общая схема: от HTML/CSS/JS до первых пикселей и точки блокировок

    Стриминг HTML и почему «первый байт» важен

    HTML обычно приходит потоком, а не целиком. Это позволяет браузеру:

  • Начать парсить документ, не дожидаясь полной загрузки
  • Рано обнаружить критичные ресурсы (CSS, шрифты, JS) и запустить их загрузку
  • Практический вывод для интервью: улучшение TTFB и ранняя отдача верхней части HTML помогают ускорить не только сеть, но и старт парсинга и обнаружение подресурсов.

    HTML-парсер и построение DOM

    От байтов к токенам и узлам

    Упрощённо браузер делает:

  • Декодирует байты в символы (учитывая кодировку)
  • Превращает поток символов в токены (теги, текст, комментарии)
  • По токенам строит дерево узлов — DOM (Document Object Model)
  • DOM — это структурированное представление документа, с которым работает JS: document.querySelector, события, изменение узлов.

    Инкрементальность DOM

    DOM строится по мере поступления HTML. Поэтому:

  • Скрипт, стоящий в середине документа, «видит» только уже распарсенную часть
  • Событие DOMContentLoaded зависит от того, когда HTML закончил парситься (подробнее ниже)
  • Справочно: Document Object Model (DOM)

    CSS: от текста к CSSOM

    Что такое CSSOM

    CSSOM (CSS Object Model) — дерево/структура, которая представляет стили документа в виде объектов и правил.

    Браузер получает CSS из:

  • Внешних файлов через link rel="stylesheet"
  • Встроенных стилей
  • Справочно: CSSOM

    Почему CSS считается render-blocking

    Чтобы нарисовать элементы корректно, браузеру нужны стили. Поэтому внешние таблицы стилей (по умолчанию) считаются render-blocking:

  • Браузер может продолжать строить DOM
  • Но первый рендер (и дальнейшие этапы вроде layout/paint) ограничены тем, что стили ещё не готовы
  • Ключевой нюанс: CSS не блокирует загрузку HTML, но может блокировать первый визуальный результат и выполнение некоторых скриптов, которым нужны вычисленные стили.

    Материал по критическому пути: Critical rendering path

    Как JavaScript вмешивается в парсинг

    Почему обычный script блокирует HTML-парсер

    Когда HTML-парсер встречает script без специальных атрибутов, браузер обычно:

  • Останавливает парсинг HTML
  • Догружает скрипт (если он внешний)
  • Выполняет скрипт
  • Продолжает парсинг
  • Причина простая: скрипт может вызвать document.write, изменить DOM, добавить стили и так далее. Чтобы сохранить корректность, браузер делает выполнение синхронным относительно парсинга.

    Справочно: Элемент script

    async и defer: что меняется

    Главная тема на собеседованиях — как атрибуты скрипта влияют на порядок загрузки/выполнения.

    | Атрибут | Загрузка | Выполнение | Влияние на парсинг HTML | Типичный кейс | |---|---|---|---|---| | без атрибутов | во время парсинга, по встрече | сразу после загрузки | блокирует парсинг | критичный код, который должен выполниться немедленно | | async | параллельно парсингу | сразу после загрузки | может прервать парсинг в момент готовности | аналитика, виджеты, независимые скрипты | | defer | параллельно парсингу | после завершения парсинга HTML | парсинг не блокируется | основной бандл приложения, работающий с готовым DOM |

    Важные детали:

  • defer сохраняет порядок выполнения скриптов относительно друг друга (как они идут в документе).
  • async не гарантирует порядок: кто раньше загрузится, тот раньше выполнится.
  • Module-скрипты

    <script type="module"> по поведению ближе к defer: загружается параллельно и выполняется после парсинга (плюс имеет свою модель импортов и строгий режим).

    Справочно: JavaScript modules

    Взаимозависимость CSS и JS

    На практике блокировки часто накладываются:

  • Если скрипт должен вычислить размеры/стили элементов, ему нужен актуальный стиль, а значит CSS должен быть загружен и распарсен.
  • В некоторых браузерных оптимизациях встречается задержка выполнения скриптов, если перед ними есть ещё не загруженные критичные стили, чтобы скрипт не «прочитал» промежуточное состояние.
  • Практический вывод: если скрипт стоит в head и зависит от стилей, вы легко получите длинный провал до первого контента.

    Обнаружение и загрузка ресурсов

    Откуда берутся подресурсы

    Браузер узнаёт, что нужно загрузить, из нескольких источников:

  • HTML-теги: link, script, img, source, video, audio
  • CSS: @import, url(...) (фоны, шрифты)
  • JS: fetch, import(), создание Image, добавление link в DOM
  • Порядок обнаружения влияет на скорость: то, что объявлено раньше, обычно раньше попадает в очередь загрузки.

    Приоритизация: что грузится раньше

    Грубо (и упрощённо) браузер пытается как можно раньше добыть то, что нужно для первого рендера:

  • HTML документа
  • Render-blocking CSS
  • Шрифты (если они влияют на видимый текст)
  • Скрипты, которые блокируют парсер или нужны для ранней инициализации
  • Но реальный приоритет зависит от:

  • Типа ресурса и контекста (например, img в первом экране vs далеко ниже)
  • Текущей загрузки сети и количества соединений
  • Подсказок разработчика
  • Preload и другие подсказки

    Когда вы знаете, что ресурс понадобится очень рано, можно помочь браузеру.

  • link rel="preload" — сказать: загрузи как можно раньше, это точно понадобится.
  • - Справочно: rel=preload

  • fetchpriority — подсказать относительную важность, например для изображений.
  • - Справочно: fetchpriority у img

    Важный нюанс для интервью: preload полезен только если вы правильно указали as (тип ресурса) и ресурс действительно используется, иначе вы создаёте конкуренцию за сеть и можете ухудшить загрузку.

    DOMContentLoaded и load: что они означают

    DOMContentLoaded

    DOMContentLoaded срабатывает, когда:

  • HTML распарсен
  • DOM построен
  • Выполнены скрипты, которые должны выполниться до завершения парсинга (например, defer-скрипты)
  • При этом не обязательно:

  • Загружены картинки
  • Загружены стили фоновых изображений
  • Завершены все сетевые запросы
  • Справочно: DOMContentLoaded

    load

    Событие load на window — это более поздняя точка: когда загрузились все зависимые ресурсы страницы (в типичном понимании: стили, изображения, подфреймы и т.д.).

    Справочно: Window load event

    Практический вывод: измерять “страница загрузилась” по load часто слишком поздно для UX, а вот DOMContentLoaded ближе к моменту, когда приложение может начать работу с DOM.

    Типовые ошибки и правильные ментальные модели для собеседования

    Ошибка: «CSS блокирует парсинг HTML»

    Правильнее:

  • HTML обычно продолжает парситься
  • Но CSS может блокировать первый визуальный рендер и влиять на момент, когда имеет смысл выполнять код, завязанный на стили
  • Ошибка: «async быстрее defer всегда»

    Правильнее:

  • async быстрее доставляет выполнение, но ломает порядок
  • defer сохраняет порядок и обычно лучше подходит для основного бандла приложения
  • Ошибка: «DOMContentLoaded значит всё загрузилось»

    Правильнее:

  • DOM готов, но изображения и другие ресурсы могут догружаться
  • Как это проверять в DevTools

    Мини-набор действий, которые ожидают на интервью:

  • Network → включить Waterfall, посмотреть, какие ресурсы стартуют поздно
  • Performance → увидеть длительные задачи, оценить, чем занят main thread во время парсинга/выполнения
  • Elements/Styles → понять, когда применились стили
  • Справочно по Performance панели: Analyze runtime performance

    Итог

    На участке от первого байта HTML до готовности базовых структур браузер:

  • Потоково парсит HTML и строит DOM
  • Загружает и парсит CSS, строит CSSOM
  • Выполняет JavaScript, который может блокировать парсер (обычные script) или выполняться позже (defer) либо независимо (async)
  • Обнаруживает ресурсы из HTML/CSS/JS и приоритизирует их загрузку
  • Сигнализирует ключевыми событиями DOMContentLoaded и load
  • В следующей части курса логично перейти к тому, как DOM и CSSOM превращаются в пиксели, и где в этом пайплайне возникают reflow, repaint и composite, а также какие операции JS заставляют браузер перерасчитывать геометрию и перерисовывать страницу.

    3. Рендеринг: style, layout(reflow), paint(repaint), compositing и GPU

    Рендеринг: style, layout(reflow), paint(repaint), compositing и GPU

    Как эта статья продолжает пайплайн из предыдущих частей

    В прошлых статьях мы дошли до момента, когда браузер:

  • Получил HTML (TTFB и сетевой слой)
  • Распарсил HTML и построил DOM
  • Загрузил и распарсил CSS и построил CSSOM
  • Выполнил JavaScript и обнаружил ресурсы
  • Дальше начинается превращение структур данных (DOM и CSSOM) в пиксели на экране. На собеседованиях это обычно спрашивают через термины reflow/layout, repaint/paint, composite и через практику: какие свойства и методы в JS заставляют браузер пересчитывать геометрию и перерисовывать страницу.

    !Обзорная схема того, как DOM и CSSOM превращаются в пиксели

    Базовая модель: что именно браузер рендерит

    Render tree: что попадает в отрисовку

    Браузеру недостаточно иметь DOM и CSSOM отдельно. Он строит структуру, которая участвует в отрисовке (часто называют render tree):

  • Берутся DOM-узлы, которые должны отображаться
  • Применяются вычисленные стили из CSSOM
  • Узлы, которые не рисуются, исключаются
  • Классический пример:

  • display: none исключает элемент из рендера целиком (нет layout и paint для него)
  • visibility: hidden оставляет место в layout, но элемент не рисуется (layout есть, paint нет)
  • Invalidation: почему браузер не пересчитывает всё всегда

    Если бы браузер пересчитывал стили, геометрию и рисование для всей страницы при любом изменении, интерфейсы были бы очень медленными.

    Поэтому используется подход:

  • изменение в DOM или стилях создаёт инвалидации (что именно стало неактуальным)
  • браузер старается пересчитать только затронутую часть
  • пересчёты обычно батчатся и выполняются перед следующим кадром
  • Этапы рендеринга

    Ниже — ментальная модель, которую удобно проговаривать на интервью. Реальные движки сложнее, но эти этапы сохраняются.

    Style: вычисление стилей

    Style calculation — это вычисление итоговых стилей для элементов.

    Что происходит:

  • Каскад: выбор победивших правил (учёт специфичности, порядка, важности)
  • Наследование: применение наследуемых свойств от родителей
  • Вычисление значений: приведение em, rem, процентов и других единиц к вычисленному виду
  • Что важно:

  • Изменение class или инлайнового style почти всегда требует пересчёта стилей
  • Style сам по себе ещё не означает пересчёт геометрии: часть свойств влияет только на рисование или композицию
  • Справочно:

  • MDN: Critical rendering path
  • Layout (reflow): расчёт геометрии

    Layout (часто на интервью говорят reflow) — это вычисление:

  • размеров боксов
  • их позиций
  • переносов строк, размеров текста, таблиц
  • зависимостей между элементами (поток документа)
  • Почему layout дорогой:

  • геометрия элементов часто зависит от соседей и родителей
  • изменение одного блока может «потянуть» пересчёт большого поддерева
  • чтение геометрии из JS может принудительно завершить незаконченные пересчёты (см. ниже про forced synchronous layout)
  • Терминология:

  • layout и reflow в разговорной практике почти синонимы
  • в документации движков чаще используется layout
  • Paint (repaint): рисование

    Paint (часто на интервью говорят repaint) — это этап, на котором браузер превращает элементы в команды рисования (условно: нарисуй фон, нарисуй текст, нарисуй границу, нарисуй тень) и затем растеризует результат.

    Paint обычно требуется, когда меняется внешний вид без изменения геометрии:

  • background-color
  • color
  • box-shadow
  • border-color
  • outline
  • Важно:

  • repaint не обязательно означает layout
  • но layout почти всегда влечёт paint для затронутых областей
  • Compositing: сборка слоёв

    Compositing — это сборка финального кадра из отдельных поверхностей (слоёв) и применение трансформаций.

    Простая модель:

  • часть элементов может быть поднята в отдельные слои
  • эти слои можно двигать/прозрачивать без перерисовки их содержимого
  • компоновщик (compositor) собирает кадр и отдаёт на вывод
  • Типичный выигрыш:

  • анимации transform и opacity часто можно выполнять на уровне compositing, без layout и paint
  • Справочно:

  • web.dev: Rendering performance
  • !Иллюстрация разницы между repaint и composite

    GPU и что именно ускоряется

    Важно разделять:

  • GPU не делает магически всё быстрее
  • GPU особенно полезен там, где задача сводится к массовым параллельным операциям над пикселями и текстурами
  • В терминах браузера GPU часто участвует в:

  • растеризации (в зависимости от движка и настроек)
  • компоновке слоёв (compositing)
  • применении transform, opacity, клиппинга и некоторых эффектов на уровне текстур
  • Практический вывод:

  • свойства, которые можно обработать на этапе compositing (например, transform), обычно дают более плавные анимации
  • но лишние слои увеличивают потребление памяти и могут ухудшить ситуацию
  • Справочно:

  • Chrome Developers: GPU acceleration
  • Три категории изменений: layout, paint, composite-only

    Для интервью удобно классифицировать изменения стилей на 3 группы по самой дорогой стадии, которую они вызывают.

    Таблица: типовые свойства и какой этап они затрагивают

    | Категория | Что происходит | Примеры свойств | Типичный риск | |---|---|---|---| | Layout (reflow) | Пересчёт геометрии, затем обычно paint | width, height, padding, margin, border-width, display, font-size, line-height, position, top/left (в потоке) | Большие каскадные пересчёты, лаги при скролле/ресайзе | | Paint (repaint) | Перерисовка без изменения геометрии | background, color, box-shadow, border-color, outline, text-decoration | Частая перерисовка больших областей | | Composite-only | Без layout и часто без paint, меняется сборка слоёв | transform, opacity | Лишние слои и память, но обычно лучшая плавность анимаций |

    Оговорки, которые полезно проговорить на собеседовании:

  • одна и та же правка может приводить к разным последствиям в зависимости от контекста (например, top для position: absolute и transform: translate часто различаются по стоимости)
  • движки оптимизируют, но общая логика «layout дороже paint, paint дороже composite» обычно верна
  • Что в JS вызывает layout: чтение и запись

    Forced synchronous layout: почему чтение размеров может быть дорогим

    Браузер старается откладывать пересчёты до конца текущей задачи и выполнять их пачкой перед кадром. Но некоторые операции в JS требуют точного ответа прямо сейчас. Тогда браузер вынужден:

  • применить все накопленные изменения стилей
  • выполнить layout
  • вернуть актуальное значение
  • Это называют forced synchronous layout (в быту часто говорят forced reflow).

    Справочно:

  • web.dev: Avoid large, complex layouts and layout thrashing
  • Типичные чтения, которые могут форсировать layout

    Важно: сами по себе эти операции не всегда вызывают layout, но если есть грязные изменения, они часто заставят браузер «досчитать» геометрию.

  • element.getBoundingClientRect()
  • element.offsetWidth, element.offsetHeight, element.offsetTop, element.offsetLeft
  • element.clientWidth, element.clientHeight
  • element.scrollWidth, element.scrollHeight, element.scrollTop (чтение/запись зависит от ситуации)
  • window.getComputedStyle(element) (особенно если затем читаются вычисленные геометрические значения)
  • Справочно:

  • MDN: Element.getBoundingClientRect
  • MDN: Window.getComputedStyle
  • Записи, которые часто приводят к layout

  • изменения, влияющие на геометрию: width, height, padding, margin, display
  • вставка/удаление элементов в DOM в местах, где меняется поток
  • изменение текста (может менять размеры строк и контейнеров)
  • Layout thrashing: самый частый анти-паттерн

    Layout thrashing — это ситуация, когда код чередует:

  • запись, которая пачкает layout (например, меняет ширину)
  • чтение геометрии (например, offsetWidth)
  • снова запись
  • снова чтение
  • В результате браузер вынужден выполнять layout много раз в рамках одного кадра, вместо одного батча.

    Типичная стратегия исправления:

  • сначала собрать все чтения (measure)
  • затем выполнить все записи (mutate)
  • Этот паттерн часто всплывает на интервью в виде вопроса: почему анимация лагает, хотя вы меняете всего пару свойств.

    Как браузер решает, что станет отдельным слоем

    Движок создаёт слои по внутренним правилам. В среднем вам важно понимать:

  • некоторые эффекты и типы позиционирования повышают шанс, что элемент окажется в отдельном слое
  • отдельный слой помогает, когда элемент часто меняется на этапе compositing
  • слишком много слоёв создают накладные расходы
  • Инструменты:

  • в Chrome DevTools можно включать визуализацию слоёв и перерисовок
  • Справочно:

  • Chrome DevTools: View layers
  • will-change: подсказка браузеру

    will-change позволяет заранее сообщить браузеру, что свойство скоро начнёт часто меняться.

  • плюс: браузер может подготовить оптимизации (например, слой)
  • минус: злоупотребление приводит к лишним слоям и расходу памяти
  • Справочно:

  • MDN: will-change
  • Repaint, reflow, composite: как это спрашивают на собеседовании

    Полезный ответ в 20–30 секунд:

  • Style: пересчитываем итоговые стили.
  • Layout (reflow): пересчитываем размеры и позиции.
  • Paint (repaint): рисуем пиксели для элементов.
  • Composite: собираем слои и выводим кадр; transform и opacity часто могут обновляться здесь.
  • И дальше сразу практический пример:

  • изменение width почти всегда вызывает layout
  • изменение background-color обычно вызывает paint
  • изменение transform: translateX(...) часто можно сделать на composite-only и получить более плавную анимацию
  • Как это проверять в DevTools

    Минимальный набор, который стоит уметь делать руками:

  • Performance запись: увидеть, где время ушло в Recalculate Style, Layout, Paint, Composite
  • Rendering инструменты: включить подсветку перерисовок (paint flashing) и увидеть, какие зоны реально перерисовываются
  • Справочно:

  • Chrome DevTools: Performance
  • Chrome DevTools: Rendering tools
  • Итог

    Рендеринг в браузере можно объяснить как последовательность стадий:

  • Style: вычислить итоговые стили
  • Layout (reflow): вычислить геометрию
  • Paint (repaint): нарисовать пиксели
  • Compositing: собрать слои и вывести кадр (часто с участием GPU)
  • Для производительности фронтендера ключевое:

  • минимизировать частые layout-операции и особенно forced layout
  • избегать layout thrashing (чередования read/write)
  • по возможности анимировать через transform и opacity, понимая компромиссы со слоями и памятью
  • 4. Event Loop и асинхронность: задачи, микрозадачи, рендер-тик

    Event Loop и асинхронность: задачи, микрозадачи, рендер-тик

    Зачем это нужно в контексте браузерного пайплайна

    В предыдущих статьях мы прошли путь:

  • от URL и кешей до получения HTML
  • через парсинг HTML/CSS/JS и построение DOM/CSSOM
  • до рендеринга: style → layout (reflow) → paint (repaint) → compositing
  • Но на практике главный вопрос на собеседованиях звучит так: когда именно выполняется JavaScript относительно рендера и пользовательского ввода.

    Ответ на него — Event Loop: модель, по которой браузер планирует выполнение JS, обработку событий, сетевых колбэков и вставляет между ними рендер-тики.

    Основные источники для подготовки:

  • web.dev: The event loop
  • MDN: Concurrency model and the event loop
  • !Общая ментальная модель: JS выполняется задачами, промисы — микрозадачами, а рендер вставляется между ними

    Базовые понятия: стек, задачи и микрозадачи

    Стек вызовов и почему он должен опустеть

    JavaScript на главном потоке браузера выполняется в call stack (стеке вызовов). Пока стек не пуст:

  • браузер не может начать выполнять следующую задачу
  • браузер обычно не может начать следующий рендер-тик на главном потоке
  • Поэтому длительный синхронный код создаёт long task и «замораживает» интерфейс: события не обрабатываются, кадры не рисуются.

    Что такое задача (task)

    Task (часто в разговоре говорят macrotask) — это единица работы, которую event loop берёт из очереди задач.

    Типичные источники задач в браузере:

  • setTimeout, setInterval
  • обработчики событий DOM (например, click, keydown)
  • завершение некоторых операций Web API (упрощённо: «колбэк вернулся в JS»)
  • сообщение postMessage
  • Справочно:

  • MDN: setTimeout
  • Что такое микрозадача (microtask)

    Microtask — это задача более высокого приоритета, которая выполняется после текущей задачи, но до того, как event loop возьмёт следующую задачу.

    Основные источники микрозадач:

  • реакции промисов: .then, .catch, .finally
  • queueMicrotask
  • MutationObserver
  • Справочно:

  • MDN: queueMicrotask
  • MDN: MutationObserver
  • Главный цикл: что происходит «между кадрами»

    Для интервью очень полезна короткая формулировка:

  • Event loop берёт одну задачу из очереди задач и выполняет её до конца.
  • Затем выполняет все микрозадачи, которые накопились.
  • Затем браузер получает шанс выполнить render step: пересчитать стили, сделать layout, paint и compositing, если есть изменения.
  • Затем берётся следующая задача.
  • Критичный нюанс: микрозадачи выполняются до тех пор, пока очередь микрозадач не станет пустой. Это означает, что микрозадачами можно случайно вытеснить рендер, если бесконечно планировать новые микрозадачи.

    Порядок выполнения: разбор на примере

    Код ниже проверяет базовую дисциплину очередей:

    Ожидаемый порядок:

  • A
  • B
  • promise 1
  • promise 2
  • timeout
  • Почему так:

  • Синхронный код выполняется прямо сейчас (стек).
  • setTimeout ставит колбэк в очередь задач (не «выполнить сразу», даже при 0).
  • then ставит реакцию промиса в очередь микрозадач.
  • После завершения текущей задачи (в данном случае — выполнение всего скрипта) движок вычищает микрозадачи.
  • Только затем берётся следующая задача — setTimeout.
  • Справочно:

  • MDN: Promise
  • async/await как синтаксис над промисами

    async/await на интервью важно объяснять не «магией», а как удобный синтаксис над промисами.

    Порядок будет:

  • A
  • 1
  • B
  • 2
  • Почему:

  • до await выполнение синхронное
  • await «разрывает» функцию: продолжение после await планируется как микрозадача (через механизм промисов)
  • Это напрямую связано с практикой: код после await не выполнится в текущем стеке, даже если вы await-ите уже готовый промис.

    Рендер-тик: где в этом месте появляется style/layout/paint/composite

    Что такое «render step» и почему он не обязан происходить после каждой задачи

    Рендеринг — дорогой процесс. Поэтому браузер обычно не рендерит «после каждой строчки JS», а старается:

  • батчить изменения DOM/CSS
  • выполнить пересчёты ближе к кадру
  • Ментальная модель:

  • JS меняет DOM/CSS → браузер помечает части как «грязные»
  • в конце задачи и микрозадач браузер решает, нужен ли рендер
  • если нужен, выполняет этапы из прошлой статьи: style → layout → paint → compositing
  • Важное следствие: DOM меняется сразу, а пиксели обычно обновляются на следующем рендер-тике.

    requestAnimationFrame: код «прямо перед кадром»

    requestAnimationFrame (сокращённо rAF) — API для планирования функции на ближайший кадр, перед отрисовкой.

    Типичные применения:

  • плавные анимации, синхронизированные с кадрами
  • паттерн measure-then-mutate для борьбы с layout thrashing
  • Справочно:

  • MDN: requestAnimationFrame
  • Практическая ментальная модель для интервью:

  • setTimeout(..., 0) попадёт в очередь задач и выполнится «когда дойдёт очередь»
  • requestAnimationFrame — это «вставка около рендера», чтобы работать в ритме кадров
  • Forced synchronous layout и связь с event loop

    Из прошлой статьи про рендеринг у нас был ключевой баг-паттерн: forced synchronous layout.

    Он особенно хорошо объясняется через event loop:

  • в рамках одной задачи вы сделали записи в DOM/CSS (браузер отложил layout)
  • затем вы читаете геометрию (offsetWidth, getBoundingClientRect())
  • браузеру нужно вернуть точный ответ прямо сейчас, поэтому он форсирует пересчёты (style/layout)
  • Пример опасного чередования:

    Один из типовых способов снизить риск — группировать чтения и записи, иногда в связке с rAF:

    Это не «магическое лечение», но полезная дисциплина: измерения ближе к кадру, изменения — тоже батчом.

    Частые ловушки, которые любят на собеседованиях

    Ловушка: «микрозадачи — это просто маленькие задачи»

    Важно проговорить отличие:

  • задачи выполняются по одной, между ними браузер может вставлять рендер
  • микрозадачи вычищаются полностью перед тем, как перейти к следующей задаче и перед тем, как дать шанс рендеру
  • Следствие: бесконечная цепочка микрозадач способна «съесть» время и ухудшить отзывчивость.

    Ловушка: «setTimeout(0) — выполнится сразу после текущей строки»

    Нет: он выполнится не раньше, чем завершится текущая задача и микрозадачи, и когда event loop дойдёт до следующей задачи.

    Ловушка: «Promise.then выполнится синхронно, если промис уже resolve»

    Нет: реакции промиса всегда планируются как микрозадачи.

    Как это проверять в DevTools

    Что стоит уметь сделать руками:

  • В Performance записи увидеть:
  • - длинные задачи (scripting) - участки Recalculate Style, Layout, Paint, Composite между задачами
  • По логам/markers понять, что выполняется в задаче, что в микрозадаче, и где происходят кадры
  • Справочно:

  • Chrome DevTools: Performance
  • Итог

    Для браузерной асинхронности полезна простая, но точная модель:

  • Задачи (task): события, таймеры и другие «внешние» поводы выполнить JS
  • Микрозадачи (microtask): в первую очередь промисы; выполняются сразу после текущей задачи и до следующей
  • Рендер-тик: шанс браузеру применить изменения и пройти этапы style/layout/paint/composite между задачами
  • Эта модель связывает воедино темы курса:

  • почему промисы влияют на порядок выполнения
  • почему длительные задачи и цепочки микрозадач вредят отзывчивости
  • почему чтение геометрии в неправильный момент вызывает forced layout
  • почему requestAnimationFrame помогает синхронизироваться с кадром
  • 5. Promises и генераторы: устройство, протоколы, ошибки и типовые вопросы

    Promises и генераторы: устройство, протоколы, ошибки и типовые вопросы

    Как эта тема связана с браузерным пайплайном

    В предыдущей статье про Event Loop мы зафиксировали ключевую модель:

  • JavaScript выполняется задачами (task)
  • реакции промисов выполняются микрозадачами (microtask)
  • между задачами браузер получает шанс на рендер-тик (style → layout → paint → composite)
  • Промисы и генераторы на собеседовании проверяют не как синтаксис, а как механизмы управления исполнением: что выполняется немедленно, что откладывается, как распространяются ошибки, и как это влияет на отзывчивость интерфейса.

    !Диаграмма порядка: task → microtasks → render step

    Promises: что это такое на уровне механики

    Promise — объект, который представляет результат асинхронной операции: либо успешный, либо с ошибкой, либо ещё не готов.

    Состояния промиса

    У промиса есть три состояния:

  • pending — ещё не завершён
  • fulfilled — успешно завершён, есть значение
  • rejected — завершён с ошибкой
  • Переход возможен только один раз: из pending в fulfilled или rejected.

    Справочно: MDN: Promise

    Executor и синхронность создания

    Важный факт для интервью: функция-executor в new Promise((resolve, reject) => { ... }) запускается синхронно.

    Логи: A, B, C.

    Но обработчики .then/.catch/.finally всегда выполняются асинхронно через микрозадачи.

    Логи: A, B, then.

    Как промисы попадают в микрозадачи

    Когда промис переходит в fulfilled или rejected, движок:

  • ставит в очередь микрозадач выполнение обработчиков, которые были подписаны через .then/.catch/.finally
  • гарантирует, что эти обработчики не выполнятся “внутри текущего стека”
  • Это напрямую связывает промисы с темой рендера: длинная цепочка микрозадач способна отодвинуть момент, когда браузер сможет выполнить рендер-тик.

    Справочно: MDN: microtask

    Цепочки .then: почему они работают как конвейер

    Ключ: .then возвращает новый Promise.

    Правила:

  • если из .then вернуть обычное значение, новый промис станет fulfilled с этим значением
  • если вернуть промис, новый промис “подождёт” его и перейдёт в то же состояние
  • если бросить исключение (throw), новый промис станет rejected
  • Thenable assimilation

    На интервью часто спрашивают: “что будет, если вернуть объект с .then?”.

    Thenable — объект, похожий на промис: у него есть метод then. По правилам промисов такой объект может быть “усвоен” как асинхронный результат.

    Практический вывод:

  • промисы доверяют контракту thenable
  • некорректные thenable могут приводить к странным зависаниям или двойным вызовам, поэтому в продакшене лучше полагаться на реальные Promise
  • Ошибки в промисах: где они теряются и как их ловить

    throw внутри .then и reject

    Эти два варианта эквивалентны по эффекту для цепочки:

    Почему try/catch не ловит асинхронные ошибки

    Типовая ловушка:

    try/catch ловит только то, что происходит в текущем синхронном стеке. Отказ промиса будет обработан позже как микрозадача.

    Правильно:

    finally: что он делает и чего не делает

    finally:

  • выполняется и при успехе, и при ошибке
  • не получает значение “как аргумент” (в отличие от then/catch)
  • пропускает исходный результат дальше, если вы явно не возвращаете другое
  • Если в finally бросить ошибку или вернуть rejected-промис, цепочка станет rejected.

    Справочно: MDN: Promise.prototype.finally

    Unhandled rejections в браузере

    Если промис отклонён и к нему не присоединили обработчик ошибки, в браузере возникают события:

  • unhandledrejection
  • rejectionhandled
  • Это важно на собеседованиях как признак зрелости: ошибки промисов нужно завершать обработчиком, иначе вы получите шум в консоли и потенциально пропущенные сбои.

    Справочно: MDN: unhandledrejection

    Комбинаторы: типовые вопросы и тонкости

    Promise.all

  • резолвится, когда все промисы успешно завершились
  • реджектится при первой же ошибке
  • порядок результатов соответствует порядку входного массива
  • Справочно: MDN: Promise.all

    Promise.allSettled

  • ждёт завершения всех
  • никогда не реджектится из-за одной ошибки
  • возвращает массив объектов со статусами
  • Справочно: MDN: Promise.allSettled

    Promise.race и Promise.any

  • race завершится первым завершившимся (и успехом, и ошибкой)
  • any вернёт первый успешный; если все упали — ошибка AggregateError
  • Справочно:

  • MDN: Promise.race
  • MDN: Promise.any
  • Типовая ловушка: “all отменяет остальные?”

    Нет. Если один промис в Promise.all упал, итоговый промис станет rejected, но остальные операции продолжат выполняться сами по себе.

    Практический ответ на интервью:

  • Promises не имеют встроенной отмены
  • для отмены обычно используют AbortController в тех API, которые его поддерживают (например, fetch)
  • Справочно: MDN: AbortController

    async/await: как объяснять без магии

    async/await — синтаксис над промисами.

    Факты, которые часто проверяют:

  • async function всегда возвращает Promise
  • await приостанавливает функцию, а продолжение планируется как микрозадача
  • try/catch вокруг await ловит rejected-промис
  • Справочно: MDN: async function

    Генераторы: что это и зачем они на интервью

    Генератор — функция, которая может приостанавливать выполнение на yield и продолжать позже. На интервью генераторы — это проверка понимания:

  • итераторов и итерируемых объектов
  • протокола next(value) / { value, done }
  • управления потоком и передачи значений внутрь генератора
  • Справочно: MDN: function (generator)

    Протоколы Iterable и Iterator

    Iterable protocol

    Объект итерируемый, если у него есть метод Symbol.iterator, который возвращает итератор.

    Справочно: MDN: Iteration protocols

    Iterator protocol

    Итератор — объект с методом next(), который возвращает объект вида:

  • value — очередное значение
  • done — признак завершения
  • Пример ручного итератора:

    Генератор как итератор “из коробки”

    Генераторная функция создаёт объект, который:

  • одновременно является итератором (есть next)
  • и итерируемым (есть Symbol.iterator, возвращающий самого себя)
  • Нюанс, который любят: return генератора не попадает в for...of, потому что цикл останавливается на done: true.

    Управление генератором: next, throw, return

    Передача значения внутрь через next(value)

    Значение, переданное в next(value), становится результатом предыдущего yield.

    Ошибки: iterator.throw(error)

    Можно “вбросить” ошибку внутрь генератора:

    Принудительное завершение: iterator.return(value)

    return завершает итерацию сразу.

    yield*: делегирование итерации

    yield* “прокидывает” элементы другого итерируемого объекта.

    Генераторы и асинхронность: где здесь промисы

    Обычные генераторы сами по себе не делают код асинхронным. Но исторически их использовали для “похожего на async/await” стиля: генератор yield-ит промис, а внешний раннер ждёт его и делает next.

    Сегодня на интервью важно:

  • понимать, что async/await проще и стандартнее
  • но понимать идею “пауз” и “возобновления” полезно для объяснения протоколов
  • Async generators и for await...of

    Есть асинхронные генераторы: async function*. Они возвращают async iterator, у которого next() возвращает промис.

    Справочно:

  • MDN: async function
  • MDN: for await...of
  • Типовые вопросы на собеседовании и как отвечать

    Про промисы

  • Чем Promise.resolve(value) отличается от new Promise(resolve => resolve(value))?
  • - Promise.resolve не создаёт лишний executor и умеет усваивать thenable.
  • Почему .then всегда асинхронный?
  • - Реакции промиса выполняются как микрозадачи.
  • Почему try/catch “не ловит промисы”?
  • - Потому что ошибка/отказ происходит не в текущем синхронном стеке.
  • Что вернёт Promise.all, если один упал?
  • - Итоговый промис отклонится, но остальные операции не отменятся.

    Про генераторы и протоколы

  • Что такое iterable и iterator?
  • - Symbol.iterator создаёт итератор; итератор имеет next(){ value, done }.
  • Чем генератор удобнее ручного итератора?
  • - Он автоматически реализует протокол итератора и управляет состоянием пауз.
  • Что делает yield*?
  • - Делегирует выдачу значений другому итерируемому.

    Итог

    Промисы и генераторы в контексте браузера полезно держать в связке с Event Loop:

  • промисы планируют продолжения через микрозадачи, что влияет на порядок выполнения и момент рендера
  • цепочки .then работают как конвейер благодаря тому, что каждый .then возвращает новый промис и может усваивать thenable
  • ошибки в промисах требуют явной обработки (catch), иначе возникают unhandledrejection
  • генераторы проверяют знание итерационных протоколов и умение управлять потоком (next/throw/return, yield*)
  • асинхронные генераторы связывают генераторную модель с промисами через for await...of