Как стать автором
Поиск
Написать публикацию
Обновить
522.16
OTUS
Развиваем технологии, обучая их создателей

103 Early Hints в NGINX: как выжать бесплатный прирост LCP без переписывания бэкенда

Уровень сложностиСредний
Время на прочтение8 мин
Количество просмотров1.3K

Привет, Хабр!

Вы, наверно, привыкли к стандартным 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 (их можно даже хранить в БД или генерировать автоматически по зависимостям).

Нюансы

  1. HTTP/1.1. Старые браузеры, которые не ждут коды 103, просто не знают, что делать с дополнительным ответом. Поэтому включать подсказки для них опасно. Хорошая практика – проверять протокол, sec-fetch и т.д., как в примере выше. Если map дал пустую строку — early_hints не сработает и клиент не увидит ничего лишнего.

  2. Навигация только. Early Hints предназначен только для навигационных запросов (Sec-Fetch-Mode: navigate). Никакие AJAX-запросы, загрузка изображений в фоне, вызовы с mode: 'cors' и даже iFrame-подзапросы не получат 103 (и не должны).

  3. Содержимое подсказок. Важно не переусердствовать с hint-ами, чтобы не породить конфликт версий. Если вы предсказали main.abcd100.css, а в основной HTML оказывается main.abcd105.css, браузер зря потратит время (и диск/трафик). Поэтому даем только достаточно стабильные ресурсы: например, общий базовый стиль, фавикон, шрифт. Google советует не пытаться подтягивать «динамически генерируемые части» в 103. Разумно разбить ресурсы на «стабильную часть» (для подсказок) и «экспериментальную», которая подтянется в основном документе.

  4. Повторная линковка. Даже если мы отправили Link: rel=preload в 103, хорошо бы также указать эти же (и, возможно, дополнительно другие) Link в окончательном ответе (200 OK) или в тегах <link rel=preload> HTML. Дело в том, что у части клиентов Early Hints может не сработать (например, если оно проскакивает плагин кеширования, CDN и т. д.), или если нужный ресурс не стал критическим. Поэтому продублируйте важные preload-ссылки в финальном HTML. Как раз из примера Google видно: после 103 и 200 OK оба содержат Link.

  5. HTTP/2 Push vs Early Hints. Если вы раньше пользовались HTTP/2 Server Push, знайте: Early Hints – более лёгкая альтернатива. При Push данные насильно шли от сервера, иногда избыточно. А Early Hints – только намёк браузеру, без навязывания. Современные браузеры благосклоннее воспринимают link preload, чем push. Но в любом случае, как показала практика, именно 103 умеет запускать загрузку прямо во время генерации страницы, а не после, как обычные <link> в HTML.

  6. Отладка и проверка. Увидеть работу 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, легко сводят на нет все усилия по оптимизации. Если хотите прокачаться глубже и убрать эти «слепые зоны», приходите на бесплатные практические занятия:

Освоить актуальные протоколы маршрутизации и научиться предотвращать и устранять проблемы, возникающие в сетях, можно на курсе "Network Engineer. Professional". Пройдите вступительный тест, чтобы проверить свой уровень знаний.

Чтобы оставаться в курсе актуальных технологий и трендов, подписывайтесь на Telegram-канал OTUS.

Теги:
Хабы:
+13
Комментарии3

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS