awesome-everything EN
↑ Обратно к восхождению

Браузер и фронтенд-рантайм

Восемь слоёв трассировки: от service worker до второй навигации

Суть Одна страница товара e-commerce, прослеженная через все восемь слоёв — сеть, парсинг HTML, CSS, V8, первая отрисовка, гидратация, взаимодействие, вторая навигация — с бюджетными числами для каждого.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 18 min

Одна страница товара e-commerce: серверно-отрендеренная, React-based, hero-картинка, кнопка «В корзину», зарегистрированный service worker. Средний Android-телефон, 4G. Что именно происходит — в каком порядке, на каком потоке, тратя сколько времени — от момента, когда пользователь тапает ссылку, до обновления значка корзины?

Страница, которую мы трассируем.

Одна конкретная страница: страница товара e-commerce, серверно-отрендеренная, на React, с hero-картинкой, ценой, кнопкой «В корзину», секцией отзывов ниже сгиба и service worker, зарегистрированным с прошлого визита. Пользователь на среднем Android-телефоне на 4G. Мы следуем за этой страницей от момента, когда он тапает ссылку, до момента, когда он успешно добавляет товар в корзину.

Слой 1 — Сеть.

Тап триггерит навигацию. DNS резолвит хост, TLS-рукопожатие защищает соединение, HTTP-запрос уходит. Но есть нюанс: service worker с прошлого визита зарегистрирован. Браузер даёт fetch-обработчику service worker первый взгляд — он может отдать навигацию из кеша или пропустить в сеть. В любом случае первый байт HTML приходит. Это TTFB, и это пол под всем: ничего ниже по потоку не может стартовать до прихода байтов.

Слой 2 — Парсинг HTML и preload-сканер.

HTML-парсер начинает превращать байты в DOM-дерево, сверху вниз. Впереди него мчится preload-сканер, замечая ссылки <img>, <script> и <link> и запуская их загрузки рано. Поэтому hero-картинка, сидящая в начальном HTML как обычный <img>, начинает скачиваться немедленно — часы LCP уже тикают, и обнаружение происходит так рано, как возможно. Render-blocking <script> в <head> поставил бы главный парсер на паузу здесь; defer-нутый — нет.

Слой 3 — CSS и CSSOM.

Когда ресурсы <link rel="stylesheet"> приходят, браузер парсит их в CSSOM. CSS render-blocking по дизайну: браузер не нарисует, пока у него нет CSSOM, потому что отрисовка с неполными стилями означала бы вспышку нестилизованного контента. DOM и CSSOM вместе образуют render tree. Если CSS большой или отдаётся медленно, каждая отрисовка ниже по потоку ждёт.

Слой 4 — JavaScript: парсинг и компиляция V8.

JavaScript-бандл приходит, и V8 берётся за дело — но не запуская его немедленно. V8 сначала парсит исходник и компилирует его в байткод (вход интерпретатора Ignition). Для большого бандла один этот шаг parse-and-compile — это десятки-сотни миллисекунд времени главного потока на среднем телефоне, до того как выполнилась хоть одна строка. Это скрытая цена внутри «JavaScript слишком большой»: байты — не просто загрузка, это CPU-работа.

Слой 5 — Первая отрисовка.

С готовым render tree пайплайн рендера гоняется: стили, компоновка, отрисовка, композитинг. Фото товара, цена и лейаут появляются. Hero-картинка, если она закончила скачиваться, рисуется — и это очень вероятно момент LCP, прибытие самого крупного contentful-элемента. Страница теперь выглядит готовой. Но это картинка: ни один обработчик не прицеплен, никакое состояние не живо.

Слой 6 — Гидратация.

Теперь React гоняет hydrateRoot. Он обходит то же дерево компонентов, что обошёл сервер, и вместо создания DOM-узлов усыновляет существующий серверный DOM, цепляя обработчики событий и подключая состояние и эффекты — реконсилер в режиме гидратации. Это одна длинная задача: V8 исполняет дерево компонентов, реконсилер делает свой render и commit. Пока она гоняется, главный поток заблокирован. Страница выглядела готовой на слое 5; она становится реально интерактивной, только когда гидратация доходит до каждого компонента.

Слой 7 — Взаимодействие.

Пользователь тапает «В корзину». Если он тапает до того, как гидратация дошла до этой кнопки, ввод задержан — поставлен в очередь за длинной задачей гидратации — и INP записывает большую задержку. Если он тапает после, обработчик гоняется: он обновляет состояние корзины, реконсилер перерендеривает бейдж корзины, пайплайн рендера рисует изменение. Время от тапа до этой отрисовки — один сэмпл INP.

Слой 8 — Вторая навигация и воркер.

Пользователь навигирует на checkout. Теперь service worker отрабатывает своё содержание: он отдаёт app shell из кеша, поэтому TTFB второй навигации — десятки миллисекунд вместо сетевого round trip.

Трасса, в числах (средний телефон, 4G)
TTFB — отдан сетью
~300 мс
TTFB — кеш service worker
~30 мс
Парсинг + компиляция JS (V8)
~150 мс
Длинная задача гидратации
300 мс – 1 с+
Хорошие LCP / INP / CLS
2.5 с / 200 мс / 0.1
Число канонических поломок
5, по одной на домен

Наложенный таймлайн — три дорожки, не одна последовательность.

Ключевой инсайт в том, что эти слои — не аккуратная последовательность — они перекрываются на таймлайне через три ресурса:

  • Дорожка сети занята фетчем HTML, потом CSS, потом JS-бандла, потом картинки, параллельно где протокол позволяет.
  • Дорожка главного потока занята парсингом HTML, потом компиляцией JS, потом отрисовкой, потом длинной задачей гидратации.
  • Дорожка GPU делает финальный композитинг.

Performance-трасса DevTools показывает ровно эти три дорожки. Читать медленную страницу значит находить, какая дорожка — бутылочное горлышко в момент, который важен — network-bound до первой отрисовки, main-thread-bound во время гидратации.

Параллельность: почему слои не последовательны.

Браузер не делает один шаг, ждёт, делает следующий — он держит все три ресурса занятыми всегда, когда есть работа. Пока сеть скачивает JS-бандл, главный поток уже парсит HTML, который пришёл раньше. Пока главный поток компилирует JS, сеть уже фетчит картинку. Preload-сканер существует ровно ради этой параллельности — он находит ресурсы и кидает их в сеть, пока главный парсер занят чем-то ещё.

Следствие для диагностики: «медленная страница» почти никогда не один медленный шаг. Это момент, где параллельность сорвалась, где одна дорожка стала ограничением, и две другие простаивали, ожидая её. Render-blocking скрипт — классический пример: он останавливает главный парсер, и пока он качается, главный поток простаивает, хотя мог бы парсить остальной HTML.

Викторина

На второй навигации (на checkout) TTFB падает с ~300 мс до ~30 мс. Какой слой ответственен?

Викторина

На трассе V8 тратит ~150 мс до того, как выполнилась хоть одна строка бандла. Что он делает?

Проследи
1/3

DevTools-трасса страницы товара: LCP срабатывает на 1.5 с (хорошо). Но есть длинная задача главного потока на 900 мс, стартующая на 1.6 с, и тап пользователя по «В корзину» на 2.0 с показывает INP 700 мс. Что за длинная задача, и почему INP плох, несмотря на хороший LCP?

1
Step 1 of 3
Длинная задача на 900 мс — гидратация: V8 исполняет дерево компонентов, реконсилер усыновляет серверный DOM; тап на 2.0 с приземлился внутри неё, поэтому INP записал ожидание
2
Locked
Длинная задача — сеть фетчит картинки
3
Locked
INP плох, потому что LCP был плох
Викторина

Почему чтение DevTools-трассы описывают как поиск бутылочного горлышка-«дорожки», а не горлышка-«слоя»?

Расставь шаги по порядку

Расставьте восемь слоёв жизни страницы, от тапа пользователя до второй навигации.

  1. 1 Сеть — DNS, TLS, HTTP; service worker получает первый взгляд
  2. 2 Парсинг HTML — DOM-дерево, preload-сканер обнаруживает ресурсы
  3. 3 CSS — построен CSSOM, сформирован render tree
  4. 4 V8 — JS-бандл распарсен и скомпилирован в байткод
  5. 5 Первая отрисовка — компоновка, отрисовка, композитинг; LCP срабатывает
  6. 6 Гидратация — реконсилер усыновляет серверный DOM, цепляет обработчики
  7. 7 Взаимодействие — тап обработан, re-render, INP измерен
  8. 8 Вторая навигация — service worker отдаёт app shell
Закончи аналогию

Хороший LCP и плохой INP вместе — страница рисуется быстро, но по ней нельзя тапнуть — имеет имя. Оно описывает окно, где страница — убедительная картинка, не отвечающая. Как называется это окно?

Посчитай

На трассе parse-and-compile JS-бандла в V8 занимает примерно сколько миллисекунд для типового бандла на среднем телефоне — цена до выполнения любой строки?

мс
Почему это работает

Восемь слоёв — не произвольный список; это форма, которую веб приобрёл за тридцать лет эволюции. Ранние страницы были слои 1–3 и всё: сеть, парсинг, отрисовка — статический документ. JavaScript добавил исполнение, и слой 4 (V8) стал важным. AJAX и SPA сдвинули рендеринг в браузер, и первая отрисовка перестала нести полезный контент. SSR вернул серверный рендер ради быстрой первой отрисовки — но он же ввёл слой 6, гидратацию, и вместе с ней зловещую долину. Service worker (слой 8) добавился, чтобы вернуть мгновенность повторных загрузок. Каждый слой — это решение прошлого компромисса, ставшее новым компромиссом.

Вспомните перед уходом
  1. 01
    Проследите страницу товара e-commerce от тапа пользователя до работающей кнопки «В корзину», называя каждый из восьми слоёв.
  2. 02
    Какова трёхдорожечная структура DevTools-трассы, и почему «найти горлышко-дорожку» важнее «найти медленный слой»?
  3. 03
    Почему V8 тратит ~150 мс до того, как выполнится хоть одна строка бандла, и что эта цена затрагивает?
Итог

Серверно-отрендеренная React-страница товара проходит восемь слоёв: сеть (TTFB ~300 мс, или ~30 мс из кеша service worker), парсинг HTML с preload-сканером, запускающим hero рано, CSS строит CSSOM (render-blocking), V8 parse-and-compile (~150 мс работы главного потока до выполнения), первая отрисовка и LCP, длинная задача гидратации (300 мс до 1 с+), блокирующая ранние тапы, взаимодействие (INP измерен тап-до-отрисовки) и вторая навигация из кеша. Эти восемь слоёв работают на трёх перекрывающихся дорожках — сеть, главный поток и GPU — не в одной последовательности. Параллельность — норма; горлышко — момент, когда одна дорожка застопорилась и другие ждут. Зловещая долина гидратации — хороший LCP, плохой ранний INP — структурный отказ серверно-отрендеренных приложений: страница выглядит готовой до того, как работает, и каждый тап внутри длинной задачи гидратации — задержанное взаимодействие, которое чувствует пользователь.

Связанные уроки
встречается в278
Продолжить восхождение ↑Пять канонических поломок: где производство стабильно ломается
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources6
expand
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06

Trademarks belong to their respective owners. Editorial reference only.