Запрос дошёл до сервера. В первой части от момента «запрос отправлен» до первого байта ответа прошло 310 мс — больше, чем все сетевые шаги вместе. Здесь разберём обратную дорогу: что делает сервер, пока браузер ждёт, сколько из этого можно не делать вовсе и что браузер делает с полученным HTML. Спойлер: самое большое ускорение — не в сети.
Это одиннадцатая часть цикла «Что происходит, когда открываешь сайт».
Что происходит на сервере
У этого сайта путь запроса внутри сервера такой: прокси принимает TLS и передаёт запрос в nginx, nginx смотрит в кэш страниц. Если готовая копия есть — отдаёт её сразу. Если нет — передаёт PHP: WordPress загружается, делает запросы к базе, собирает HTML из шаблонов, и только потом ответ уходит обратно.
Сравним оба пути на одной странице. Для сборки «с нуля» добавляем к адресу параметр, который кэш пропускает мимо:
$ curl … https://alekseychikun.ru/blog/ # из кэша
tls=0.125068 ttfb=0.159290 x-cache-status: HIT
tls=0.099590 ttfb=0.134298 x-cache-status: HIT
$ curl … "https://alekseychikun.ru/blog/?nocache=…" # мимо кэша
tls=0.124027 ttfb=0.323557 x-cache-status: BYPASS
tls=0.109751 ttfb=0.267440 x-cache-status: BYPASS
Вычитаем время конца TLS из времени первого байта — получаем, сколько ждали ответа на сам запрос:
| Путь | Ожидание ответа | Из него сеть |
|---|---|---|
| из кэша | 34–35 мс | 29 мс — один круг |
| сборка PHP | 158–200 мс | 29 мс, остальное — работа сервера |
Из кэша сервер тратит на ответ около 6 мс — почти всё ожидание составляет дорога туда и обратно. Сборка — 130–170 мс работы, и это на лёгкой странице блога. У интернет-магазина с фильтрами и ценами из базы та же сборка легко занимает секунду и больше.
Вот почему мы так много пишем о кэше страниц. Ни сеть, ни протокол, ни настройки TLS не дают такого выигрыша: разница между HIT и сборкой больше, чем вся сетевая часть запроса.
Ответ поехал: 8,8 КБ за один круг
Сжатая страница — 8,75 КБ. Это меньше начальной порции TCP (около 14 КБ, пятая часть), поэтому весь HTML уходит одним залпом, без ожидания подтверждений. Если бы страница весила 200 КБ, понадобилось бы ещё несколько кругов, пока TCP разгоняется. Лёгкий HTML — это не про трафик, а про число кругов.
HTML — только начало
Получив HTML, браузер не показывает страницу сразу. Он читает его сверху вниз и находит, что ещё нужно загрузить. На странице блога этого сайта:
| Что | Сколько |
|---|---|
| таблицы стилей | 4 |
| скрипты | 4 |
| картинки в разметке | 10 |
| шрифты, загружаемые заранее (preload) | 2 |
| ссылок на сторонние домены | 1 — счётчик статистики |
Все ресурсы со своего домена браузер запрашивает по тому же соединению HTTP/2 — новое рукопожатие не нужно. А для стороннего домена всё начинается сначала: DNS, TCP, TLS — ещё 100–200 мс, прежде чем придёт первый байт. Поэтому каждый внешний сервис на странице — счётчик, шрифты, виджет — стоит не только своего веса, но и нового соединения. И поэтому счётчик на этом сайте загружается не сразу, а после первого действия посетителя.
Что блокирует отрисовку
- стили в
<head>браузер ждёт обязательно: без них не нарисовать ни одного пикселя правильно; - скрипты без
deferилиasyncостанавливают разбор HTML, пока не скачаются и не выполнятся; - шрифты — текст может ждать шрифт до трёх секунд, если не задано
font-display: swap; - картинки отрисовку не блокируют, но первая крупная картинка часто и есть то, что посетитель ждёт увидеть.
Всё это — уже не сеть, а фронтенд: что и в каком порядке страница просит загрузить. Как это мерить и разгонять, подробно разобрано в нашем цикле про PageSpeed.
Где на самом деле уходит время — сводка
| Шаг | Холодный заход | Что помогает |
|---|---|---|
| DNS | 36 мс | кэш резолвера; разумный TTL |
| TCP | 28 мс | keep-alive, одно соединение HTTP/2 |
| TLS | 128 мс | ключ ECDSA, возобновление сессий |
| сервер | 130–270 мс | кэш страниц — до 6 мс |
| передача HTML | 1 круг | сжатие, лёгкий HTML |
| подресурсы | от сотен мс | меньше сторонних доменов, defer, preload |
Сервер отвечает долго, а почему — неясно?
Пришлите адрес — сравню ответ из кэша и без него и покажу, куда уходят миллисекунды на сервере.
// загружаю комментарии…