Привет, Хабр!
Вы, наверно, привыкли к стандартным HTTP-ответам – 200, 301, 404, 500 и т. д. А тут подкрался новый статус 103 – Early Hints. Это небольшой пинок браузеру: сервер шлет код 103 с заголовками Link: rel=preload
ещё до того, как сформировал основной HTML. Пока бэкенд думает над ответом, браузер параллельно начинает грузить критические ресурсы (CSS, JS, шрифты и т. д.). Звучит просто, но эффект на производительность и LCP может быть весьма значительным.
Зачем это нужно для LCP и веб-перформанса
Никому не нравится ждать. Особенно когда ждали – а наконец-то на экране отображается главный контент (изображение, большой заголовок, баннер и т.п.). В метриках Core Web Vitals время до показа крупнейшего элемента страницы очень важно, и Early Hints может заметно улучшить этот показатель.
Кроме LCP, сразу начатая загрузка полезна и для других метрик: быстрее идёт First Contentful Paint и сокращается кажущееся время до полной загрузки страницы. Фактически дополнительные миллисекунды загрузки разбросанных скриптов и стилей перестают попадать в путь рендеринга.
Для NGINX внедрение Early Hints даётся практически без допиливаний нашего приложения. Вся настройка происходит на уровне веб-сервера – никакие роуты или хэндлеры в бэкенде трогать не придется (ну или почти не придется). Проще говоря, это почти бесплатный прирост LCP: нужны только правки в конфиге NGINX, а не переписывание логики выдачи страниц.
Поддержка Early Hints в браузерах и серверах
Хорошая новость: все современные браузеры уже понимают 103 Early Hints. Chrome, Safari и Edge официально поддерживают этот код (точнее, поведение с preloading). В Firefox тоже движутся в эту сторону (примеры из devtools уже есть). А крупные CDN/провайдеры (Cloudflare, Fastly, Akamai и прочие) тоже дают ранние подсказки за нас, если активировать их фичи. Но мы сейчас не о них, а о NGINX.
С точки зрения серверов: Apache (mod_http2), H2O, Node.js (v18+) и другие уже умеют отдавать 103 или эквивалентные механизмы. В NGINX поддержка появилась с версии 1.29.0 (mainline). Именно в этой версии был введён новый директив early_hints
, который управляет отправкой 103 от обратного прокси или, в будущем, и сам по себе.
Важно помнить, что 103 – это информационный статус. Старые клиенты (особенно HTTP/1.1-браузеры, которые не ждут 103) могут сломаться: они воспримут дополнительный ответ за протоколльную ошибку. Поэтому универсально включать 103 нельзя. Нужен контроль: отсылаем ранние подсказки только тем клиентам, которые их поймут и действительно получат от них выгоду.
Настройка NGINX для ранних подсказок
Для начала убедитесь, что у вас NGINX версии 1.29.0 или новее. Если вы на стабильной ветке ниже — можно либо перейти на mainline, либо собрать NGINX из исходников с патчем (о нём ниже). Предположим, версии хватило. Теперь про конфигурацию.
Идея в том, чтобы направлять 103-ответ на клиент только при навигации главной страницы и по протоколам HTTP/2 или HTTP/3. В NGINX есть переменные $http2
и $http3
, которые равны непустой строке, когда соединение ведётся по H2/H3. И заголовок Sec-Fetch-Mode: navigate
указывает, что запрос — это основная навигация, а не, скажем, загрузка подресурса или API. Общепринятая схема такая:
# Включаем отправку 103 только для запросов навигации по HTTP/2 или HTTP/3
map $http_sec_fetch_mode $early_hints {
navigate $http2$http3;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/example.crt;
ssl_certificate_key /etc/ssl/example.key;
location / {
# Активируем Early Hints по условию из map
early_hints $early_hints;
# Проксируем запрос к бэкенду
proxy_pass http://backend\_upstream;
# Пример: передаём в заголовках основной задачи
# (сами Link-ы для ранних подсказок бэкенд должен выставлять)
# Другие настройки proxy_pass как обычно...
}
}
map
устанавливает переменную $early_hints
в непустое значение (например, «1»), если Sec-Fetch-Mode
равен navigate
и протокол HTTP/2 или HTTP/3. В location
говорим early_hints $early_hints
, то есть отправлять подсказки, только если условие истинно. Таким образом у клиентов по HTTP/1.1 или при загрузке через AJAX/iframe никаких 103 даже не будет попытки: они просто не получат ответ 103.
Директива early_hints
появилась именно в 1.29.0 и по дефолту отключена. Мы должны явно её включить (либо через on/off, либо как здесь – через непустую строку). Документация на это прямо говорит: если хоть один параметр early_hints
непустой и не «0», то ранний ответ будет отправлен.
Передача подсказок из бэкенда
Если бэкенд (будь то Django, Rails, PHP и т. д.) уже умеет отдавать заголовки Link
и 103, то NGINX просто проксирует их сквозь себя. То есть вы готовите обычный код в приложении:
HTTP/1.1 103 Early Hints
Link: </static/app.css>; rel=preload; as=style
Link: </static/app.js>; rel=preload; as=script
...затем текст страницы с 200 OK...
NGINX при proxy_pass
увидит этот 103 (и его Link
) и отправит клиенту сразу. Затем он дочитает основной ответ (200 OK) и вернёт полный HTML. Никаких сложных манипуляций с NGINX в таком случае не нужно: достаточно early_hints on; proxy_pass ...
.
Статические подсказки на уровне NGINX
А что если приложение никак не может выдать 103? Например, устаревший PHP или CMS без поддержки Early Hints. Тогда можно обойтись одним NGINX. В экспериментальном PoC для NGINX уже появились примеры статического добавления подсказок с помощью add_header
. Формат такой (заметку: для этого нужна версия с патчем или пока актуален патч PoC):
location / {
# Прямо отдаём ранние подсказки сразу с NGINX
early_hints on;
# Эти заголовки пойдут в 103 ответ
# (Патч добавляет параметр 'early' к add_header)
add_header Link "</static/app.css>; rel=preload; as=style" early;
add_header Link "</static/app.js>; rel=preload; as=script" early;
try_files $uri $uri/ =404;
}
Вручную прописали Link
для двух ресурсов. В патче слово early
означает, что эти заголовки отправятся не в окончательном ответе, а именно в Early Hints. После этого, когда придёт запрос на страницу, NGINX сразу даст клиенту:
HTTP/2 103 Early Hints
Link: </static/app.css>; rel=preload; as=style
Link: </static/app.js>; rel=preload; as=script
HTTP/2 200 OK
Content-Type: text/html
...тело страницы...
Клиент, получив 103, моментально начинает загружать app.css
и app.js
по указанному пути, в то время как сервер ещё думает над HTML.
Практически это почти то же самое, что если бы бэкенд сам добавил 103. Но плюсы в том, что весь бэкенд здесь – сам NGINX. Недостаток: пока нужно поставить патч (или дождаться, когда это выйдет в стабильную ветку). Либо можно реализовать похожее через njs
или stub-интерфейсы, но обычно проще поднять mainline-сборку.
Резюме по настройке: обновляем NGINX (1.29.0+), включаем early_hints
в нужном месте (лучше в location
главной страницы), настраиваем условия (HTTP/2/3 + Sec-Fetch-Mode), и снабжаем заголовками Link: rel=preload
либо находим способ передать их от бэкенда. Больше писать в бэкенде не придётся, разве что организовать передачу нужных Link
(их можно даже хранить в БД или генерировать автоматически по зависимостям).
Нюансы
HTTP/1.1. Старые браузеры, которые не ждут коды 103, просто не знают, что делать с дополнительным ответом. Поэтому включать подсказки для них опасно. Хорошая практика – проверять протокол, sec-fetch и т.д., как в примере выше. Если
map
дал пустую строку —early_hints
не сработает и клиент не увидит ничего лишнего.Навигация только. Early Hints предназначен только для навигационных запросов (
Sec-Fetch-Mode: navigate
). Никакие AJAX-запросы, загрузка изображений в фоне, вызовы сmode: 'cors'
и даже iFrame-подзапросы не получат 103 (и не должны).Содержимое подсказок. Важно не переусердствовать с hint-ами, чтобы не породить конфликт версий. Если вы предсказали
main.abcd100.css
, а в основной HTML оказываетсяmain.abcd105.css
, браузер зря потратит время (и диск/трафик). Поэтому даем только достаточно стабильные ресурсы: например, общий базовый стиль, фавикон, шрифт. Google советует не пытаться подтягивать «динамически генерируемые части» в 103. Разумно разбить ресурсы на «стабильную часть» (для подсказок) и «экспериментальную», которая подтянется в основном документе.Повторная линковка. Даже если мы отправили
Link: rel=preload
в 103, хорошо бы также указать эти же (и, возможно, дополнительно другие)Link
в окончательном ответе (200 OK) или в тегах<link rel=preload>
HTML. Дело в том, что у части клиентов Early Hints может не сработать (например, если оно проскакивает плагин кеширования, CDN и т. д.), или если нужный ресурс не стал критическим. Поэтому продублируйте важные preload-ссылки в финальном HTML. Как раз из примера Google видно: после 103 и 200 OK оба содержатLink
.HTTP/2 Push vs Early Hints. Если вы раньше пользовались HTTP/2 Server Push, знайте: Early Hints – более лёгкая альтернатива. При Push данные насильно шли от сервера, иногда избыточно. А Early Hints – только намёк браузеру, без навязывания. Современные браузеры благосклоннее воспринимают link preload, чем push. Но в любом случае, как показала практика, именно 103 умеет запускать загрузку прямо во время генерации страницы, а не после, как обычные
<link>
в HTML.Отладка и проверка. Увидеть работу Early Hints можно в девтулзах (в сети включите HTTP/2, отключите кэш). В Chrome для ресурса-переключения у загруженных ранних подсказок будет пометка
(Disk cache)
и инициаторearly-hints
. Если без девтулз, можно сделатьcurl --http2 -i https://сайт/
и поискать строку103 Early Hints
. Бывает, curl сам схлопывает информационные коды, поэтому удобней DevTools или прокси типа mitmproxy.
Пример
Допустим, у нас простой сайт со статикой, и мы хотим заранее отправлять браузеру CSS и JS. Бэкэнд мы вносить не будем, используем патч NGINX. Конфигурация могла бы выглядеть так:
server {
listen 443 ssl http2;
server_name static.example.com;
ssl_certificate /etc/ssl/static.crt;
ssl_certificate_key /etc/ssl/static.key;
location = / {
# Отправляем 103 сразу
early_hints on;
add_header Link "</css/style.css>; rel=preload; as=style" early;
add_header Link "</js/app.js>; rel=preload; as=script" early;
# Собственно выдача страницы
try_files /index.html =404;
}
}
Клиент получит при заходе на /
заголовки типа:
HTTP/2 103 Early Hints
Link: </css/style.css>; rel=preload; as=style
Link: </js/app.js>; rel=preload; as=script
HTTP/2 200 OK
Content-Type: text/html
...тело index.html ...
И браузер в этот момент сразу грузит /css/style.css
и /js/app.js
. В итоге LCP (где, скажем, главный баннер или крупная графика зависят от этих стилей/скрипта) может появиться заметно раньше.
Результаты и эффект на LCP
Early Hints хорошо отрабатывает именно на самых популярных страницах и навигации (например, главная страница, страницы категории). Когда к вам идут люди и первый раз открывают сайт, у них нет ничего закешировано, поэтому каждый миллисекунд на счету. Как показывали различные тесты в инете, отдача в LCP измеряется сотнями миллисекунд. В одном эксперименте без 103 картинка LCP рендерилась на 45 % медленнее по времени, чем с подсказкой. В другом сравнивании разница составляла почти 1 секунду ускорения.
Для нас это означает: пользователи увидят «главную картинку» (или текст) раньше, еще до того как полностью дозагрузятся шрифты и стили. Даже если у вас и до этого был хороший LCP (например, ставили <link rel=preload>
в HTML), Early Hints добавляет бонус за счёт параллельного начинания загрузок. И всё это — без дополнительной нагрузки на сервер в момент ответа, только время на передачу небольших заголовков 103.
Даже если вы умеете выжимать максимум из веб-сервера и доводить LCP до сотен миллисекунд, есть другая область, где скрываются узкие места — сама сеть и её протоколы. Ошибки в понимании того, как устроен стек и как работает IPv6, легко сводят на нет все усилия по оптимизации. Если хотите прокачаться глубже и убрать эти «слепые зоны», приходите на бесплатные практические занятия:
10 сентября в 20:00 — Почему протоколы маршрутизации работают не только на сетевом уровне
24 сентября в 20:00 — IPv6: всё, что нужно знать сетевому инженеру
Освоить актуальные протоколы маршрутизации и научиться предотвращать и устранять проблемы, возникающие в сетях, можно на курсе "Network Engineer. Professional". Пройдите вступительный тест, чтобы проверить свой уровень знаний.
Чтобы оставаться в курсе актуальных технологий и трендов, подписывайтесь на Telegram-канал OTUS.
francyfox
Я когда делал SSR натыкался на early hints, в некоторых статьях намекалось что он устарел. Но если сейчас вбить в гугле про http push, в блогах будут писать наоборот, мол push уже не входит в сервера. Хз.
А вообще уже было https://habr.com/ru/articles/421059/