Исчерпывающее руководство по использованию HTTP/2 Server Push
- вторник, 30 мая 2017 г. в 03:13:49
Привет! Меня зовут Александр, и я – фронтенд-разработчик в компании Badoo. Пожалуй, одной из самых обсуждаемых тем в мире фронтенда в последние несколько лет является протокол HTTP/2. И не зря – ведь переход на него открывает перед разработчиками много возможностей по ускорению и оптимизации сайтов. Этот пост посвящён как раз одной из таких возможностей – Server Push. Cтатья Джереми Вагнера показалась мне интересной, и поэтому делюсь полезной информацией с вами.
Не так давно возможности разработчиков, ориентированных на производительность, заметно изменились. И появление HTTP/2 стало, возможно, самым значительным изменением. HTTP/2 больше не является фичей, которую мы с нетерпением ждём, — он уже существует (и успешно помогает справляться с проблемами вроде блокировки начала очереди и несжатых заголовков, существующими в HTTP/1), а «в комплекте» с ним идёт Server Push!
Эта технология позволяет отправлять пользователям ресурсы сайта, прежде чем они их попросят. Это элегантный способ добиться преимущества в производительности методов оптимизации HTTP/1, таких как, например, встраивание, и избежать недостатков, связанных с этой практикой.
Из этой статьи вы узнаете всё о Server Push – от принципа её работы до решаемых ею проблем: как её использовать, как определить, работает ли она и каково её влияние на производительность, и многое другое.
Доступ к веб-сайтам всегда осуществляется по шаблону «Запрос – ответ»: пользователь отправляет запрос на удалённый сервер, который с некоторой задержкой присылает ответ с запрошенным контентом.
В первоначальном запросе к веб-серверу обычно запрашивается HTML-документ. Сервер отвечает запрошенным HTML-ресурсом. Полученный HTML-документ анализируется браузером, в результате чего из него извлекаются ссылки на другие ресурсы, такие как таблицы стилей, скрипты и изображения. После их обнаружения браузер отправляет отдельный запрос для каждого ресурса и получает соответствующие ответы.
Проблема этого механизма заключается в том, что он заставляет пользователя ждать, пока браузер обнаружит и извлечёт необходимые ресурсы уже после того, как HTML- документ загружен. Это задерживает рендеринг и увеличивает время загрузки.
У нас есть решение этой проблемы. Server Push позволяет серверу превентивно «проталкивать» ресурсы веб-сайта клиенту, прежде чем пользователь запросит их явно. То есть мы можем заранее отправить то, что, как мы знаем, понадобится пользователю для запрашиваемой страницы.
Предположим, у вас есть веб-сайт, где все страницы полагаются на стили, определённые во внешней таблице стилей именем styles.css
. Когда пользователь запрашивает index.html с сервера, мы можем отправить styles.css
сразу после того, как начнём отправлять ответ для index.html
.
Вместо того чтобы ждать, пока сервер пришлёт index.html
, а затем – пока браузер запросит и получит styles.css
, пользователю нужно лишь дождаться ответа на свой первоначальный запрос. Этот ответ будут содержать оба файла: и index.html
, и styles.css
. А это означает, что браузер может начать рендеринг страницы быстрее, чем если бы ему пришлось ждать.
Как видите, использование Server Push позволяет уменьшить время рендеринга страницы. А также – решить некоторые другие проблемы, особенно в части фронтенд-разработки.
Уменьшение количества обращений к серверу для получения критически важного контента – лишь одна из проблем, решаемых Server Push, но далеко не единственная.
Так, Server Push является подходящей альтернативой ряда антипаттернов оптимизации HTTP/1, таких как встраивание CSS и JavaScript непосредственно в HTML или использование data URI scheme для внедрения бинарных данных в CSS и HTML. Эти методы имеют ценность при оптимизации HTTP/1, поскольку они уменьшают субъективное время загрузки страницы. Это означает, что, хотя общее время загрузки страницы не может быть уменьшено, страница будет загружаться быстрее для пользователя.
Безусловно, это имеет смысл. Если вы встраиваете CSS в HTML-документ в теги <style>
, браузер может сразу применить стили к HTML, не дожидаясь их извлечения из внешнего источника. Эта концепция работает и для встроенных скриптов, и для двоичных данных при использовании data URI scheme.
Похоже, это хороший способ решить проблему, не так ли? Для HTTP/1, где у вас нет другого выбора, – конечно! Но обратной стороной медали является то, что встроенное содержимое не может быть эффективно кешировано. Если ресурс (например, таблица стилей или файл JavaScript) остаётся внешним и модульным, его можно кешировать намного эффективнее. И когда пользователь переходит на следующую страницу, требующую этот же ресурс, его можно вытащить из кеша, устранив необходимость в дополнительных запросах к серверу.
Однако, когда мы встраиваем контент, он не имеет собственного контекста кеширования – его контекст кеширования совпадает с ресурсом, в который он встроен. Возьмём, например, HTML-документ со встроенным CSS. Если политика кеширования HTML-документа заставляет всегда загружать свежую копию разметки с сервера, то встроенный CSS никогда не будет кешироваться сам по себе. Конечно, документ, в который он встроен, может быть кеширован, но другие страницы, содержащие этот же дублированный CSS, будут загружаться повторно. И даже если политика кеширования менее строгая, документы HTML обычно имеют ограниченный срок хранения. Однако это компромисс, на который мы готовы пойти при оптимизации HTTP/1. Это действительно работает, и это довольно эффективно для посещающих сайт впервые. А ведь первое впечатление часто является определяющим.
Это те проблемы, которыми и занимается Server Push. Когда вы проталкиваете ресурсы, вы получаете те же практические преимущества, что и при встраивании, но ещё сохраняете свои ресурсы во внешних файлах, которые имеют собственную политику кеширования. Правда, в этом процессе есть один нюанс, который будет рассмотрен в конце статьи. А пока давайте продолжим.
Я достаточно подробно объяснил, почему вам следует рассмотреть возможность использования Server Push, а также обозначил круг проблем, которые эта технология решает как для пользователя, так и для разработчика. Теперь поговорим о том, как она используется.
Использование Server Push обычно предполагает применение HTTP-заголовка Link
в следующем формате:
Link: </css/styles.css>; rel=preload; as=style
Заметьте, я сказал «обычно». То, что вы видите выше, на самом деле является подсказкой ресурса preload
(preload resource hint). Это отдельная, отличная от Server Push оптимизация, но большинство (не все) реализаций HTTP/2 будут проталкивать объект, указанный в заголовке Link
, содержащем подсказку ресурса preload
. Если же сервер или клиент откажутся от принятия проталкиваемого ресурса, клиент всё равно сможет инициировать досрочное извлечение указанного ресурса.
Часть заголовка as=style
не является обязательной. Она информирует браузер о типе содержимого проталкиваемого ресурса. В данном случае мы используем значение style
, чтобы указать, что объект является таблицей стилей (вы можете указать другие типы контента). Важно отметить, что пропуск значения as
может привести к тому, что браузер дважды загрузит проталкиваемый ресурс. Так что не забывайте о нём!
Теперь, когда вы знаете, как запускается проталкивание, рассмотрим, как мы можем установить заголовок Link
. Это можно сделать двумя способами:
httpd.conf
или .htaccess
для Apache);header
).Link
в настройках веб-сервераВот пример настройки сервера Apache (через файл httpd.conf
или .htaccess
) для проталкивания таблицы стилей всякий раз, когда запрашивается файл HTML:
<FilesMatch "\.html$">
Header set Link "</css/styles.css>; rel=preload; as=style"
<FilesMatch>
Здесь мы используем директиву FilesMatch
для отбора запросов к файлам, заканчивающимся на .html
. При поступлении запроса, соответствующего этому критерию, мы добавляем в ответ заголовок Link
, который даёт указание серверу протолкнуть ресурс /css/styles.css
.
Примечание: HTTP/2-модуль Apache также может инициировать проталкивание ресурсов с помощью директивы H2PushResource
. Документация для этой директивы утверждает, что этот метод может инициировать проталкивание раньше, чем при использовании заголовка Link
. В зависимости от вашей конкретной установки у вас может не быть доступа к этой функции. Тестирование производительности, показанное ниже в этой статье, использует метод заголовка Link
.
На данный момент Nginx не поддерживает HTTP/2 Server Push, и до сих пор в списке изменений программного обеспечения не указано, что его поддержка была добавлена. Это может измениться по мере развития реализации Nginx HTTP/2.
Link
в бэкенд-кодеДругой способ установить заголовок Link
– использование серверного языка. Это поможет, если вы не можете изменить настройки веб-сервера. Ниже приведён пример использования PHP-функции header
для установки заголовка Link
:
header("Link: </css/styles.css>; rel=preload; as=style");
Если ваше приложение находится на виртуальном хостинге, где нет возможности изменить настройки сервера, тогда этот метод – то, что вам нужно. Вы должны иметь возможность установить этот заголовок на любом серверном языке. Просто убедитесь, что сделали это, прежде чем приступать к отправке тела ответа, во избежание возможных ошибок времени выполнения.
Все наши примеры иллюстрируют проталкивание одного ресурса. Но что, если вы хотите протолкнуть несколько? Сделать это было бы разумно, не так ли? В конце концов, сеть состоит не только из таблиц стилей. Вот как это можно сделать:
Link: </css/styles.css>; rel=preload; as=style, </js/scripts.js>; rel=preload; as=script, </img/logo.png>; rel=preload; as=image
Для проталкивания нескольких ресурсов просто отделите каждую push-директиву запятой. Поскольку подсказки ресурсов тоже добавляются через тег Link
, используя этот синтаксис, вы можете смешивать ваши push-директивы с другими подсказками ресурсов. Вот пример смешивания с подсказкой preconnect
:
Link: </css/styles.css>; rel=preload; as=style, <https://fonts.gstatic.com>; rel=preconnect
Также допустимы несколько заголовков Link
. Вот как вы можете настроить Apache для установки нескольких заголовков Link
для запросов к HTML-документам:
<FilesMatch "\.html$">
Header add Link "</css/styles.css>; rel=preload; as=style"
Header add Link "</js/scripts.js>; rel=preload; as=script"
<FilesMatch>
Этот синтаксис более удобен, чем объединение нескольких значений, разделённых запятыми, а работает не хуже. Единственным минусом является его недостаточная компактность, но удобство стоит нескольких лишних байтов, передаваемых по сети.
Теперь, когда вы знаете, как проталкивать ресурсы, давайте посмотрим, как определить, работает ли это.
Итак, вы добавили заголовок Link
, чтобы дать указание серверу протолкнуть что-нибудь. Остаётся вопрос: как узнать, работает ли он вообще?
Это зависит от браузера. В последних версиях Google Chrome проталкиваемый ресурс можно выявить по столбцу Initiator вкладки Network в окне Developer Tools.
Кроме того, если на этой же вкладке навести курсор мыши на столбец Waterfall, мы получим подробную информацию о времени проталкивания ресурса:
Инструменты Mozilla Firefox менее наглядны в определении проталкиваемых ресурсов. Статус таких ресурсов в сетевой утилите инструментов разработчика браузера помечается серой точкой.
Если вы ищете точный способ определить, был ли ресурс протолкнут сервером, вы можете использовать клиент командной строки nghttp
для проверки ответа от HTTP/2-сервера, например:
nghttp -ans https://jeremywagner.me
Таким образом вы получите краткую информацию о ресурсах, участвующих в транзакции. Протолкнутые ресурсы будут помечены звёздочкой, например:
id responseEnd requestStart process code size request path
13 +50.28ms +1.07ms 49.21ms 200 3K /
2 +50.47ms * +42.10ms 8.37ms 200 2K /css/global.css
4 +50.56ms * +42.15ms 8.41ms 200 157 /css/fonts-loaded.css
6 +50.59ms * +42.16ms 8.43ms 200 279 /js/ga.js
8 +50.62ms * +42.17ms 8.44ms 200 243 /js/load-fonts.js
10 +74.29ms * +42.18ms 32.11ms 200 5K /img/global/jeremy.png
17 +87.17ms +50.65ms 36.51ms 200 668 /js/lazyload.js
15 +87.21ms +50.65ms 36.56ms 200 2K /img/global/book-1x.png
19 +87.23ms +50.65ms 36.58ms 200 138 /js/debounce.js
21 +87.25ms +50.65ms 36.60ms 200 240 /js/nav.js
23 +87.27ms +50.65ms 36.62ms 200 302 /js/attach-nav.js
Здесь я использовал nghttp
на своём собственном сайте, который (по крайней мере, на момент написания) проталкивал пять ресурсов. Интересующие нас ресурсы помечены звёздочкой в левой части столбца requestStart
.
Теперь, когда мы можем определить, когда проталкиваются ресурсы, давайте посмотрим, как Server Push влияет на производительность реального веб-сайта.
Измерение эффекта любого повышения производительности требует хорошего инструмента тестирования. Sitespeed.io – отличный инструмент, доступный через npm; он автоматизирует тестирование страниц и собирает ценные показатели производительности.
Итак, мы выбрали подходящий инструмент – переходим к методологии тестирования.
Я хотел измерить влияние Server Push на производительность веб-сайта. Чтобы результаты были релевантными, мне нужно было установить точки сравнения по шести отдельным сценариям. Эти сценарии разделены на два аспекта: используется HTTP/2 или HTTP/1. На серверах HTTP/2 мы измеряем влияние Server Push по ряду показателей; на серверах HTTP/1 – хотим увидеть, как встраивание ресурсов влияет на производительность по тем же показателям, поскольку встраивание должно обладать примерно теми же преимуществами, которые обеспечивает Server Push.
Рассматриваемые сценарии:
Для каждого сценарии я инициировал тестирование с помощью следующей команды:
sitespeed.io -d 1 -m 1 -n 25 -c cable -b chrome –v
https://jeremywagner.me
Если вы хотите узнать, что делает эта команда, вы можете посмотреть документацию. Если вкратце, она проверяет домашнюю страницу моего сайта по адресу https://jeremywagner.me со следующими условиями:
Для каждого теста были собраны и отображены три показателя:
First Paint Time
Это момент времени, когда страница начала отображаться в браузере. Чтобы казалось, что страница загружается быстро, этот показатель нужно уменьшать как можно больше.
DOMContentLoaded Time
Это время, когда HTML документ был полностью загружен и разобран. Синхронный JavaScript-код блокирует парсер и увеличивает этот показатель. Использование атрибута async
в тегах <script>
может помочь предотвратить блокировку парсера.
Определив параметры теста, давайте посмотрим на результаты.
Тесты проводились по шести сценариям, указанным выше, с построением графиков результатов. Давайте начнём с рассмотрения того, как каждый сценарий влияет на время начала отображения страницы:
Сначала несколько слов о том, как настроен график. Часть графика в синем цвете соответствует среднему времени. Оранжевая часть – это уровень 90%. Серая часть показывает максимальное время.
Теперь перейдём к тому, что мы видим. Самыми медленными сценариями являются веб-сайты с поддержкой HTTP/2 и HTTP/1 без каких-либо улучшений. Мы видим, что использование Server Push для CSS помогает сделать страницу в среднем примерно на 8% быстрее, чем если бы эта технология не использовалась, и даже примерно на 5% быстрее, чем встраивание CSS на HTTP/1-сервере.
Однако, когда мы проталкиваем все возможные ресурсы, картина несколько меняется: время начала отображения страницы слегка увеличивается. На HTTP/1-сервере, где мы встраиваем всё, что возможно, мы достигаем чуть меньшей производительности.
Вывод здесь очевиден: при использовании Server Push можно достичь немного лучших результатов, чем на HTTP/1 с встраиванием. Однако, когда мы проталкиваем или встраиваем много ресурсов, мы наблюдаем уменьшение отдачи.
Стоит отметить, что для посещающих сайт впервые использование Server Push или встроенных ресурсов лучше, чем отсутствие улучшений. Также необходимо упомянуть, что тесты проводятся на веб-сайте с небольшими ресурсами, поэтому этот тестовый пример может не в полной мере отражать то, что достижимо для вашего сайта.
Переходим к рассмотрению влияния каждого сценария на время DOMContentLoaded:
Тенденции здесь не сильно отличаются от той, что мы видели на предыдущем графике, за исключением одного заметного отклонения: пример, в котором мы встраиваем все возможные ресурсы на HTTP/1, даёт довольно низкое время DOMContentLoaded. Вероятно, это связано с тем, что встраивание уменьшает количество ресурсов, необходимых для загрузки, что позволяет парсеру продолжать свою работу без перерывов.
Наконец, давайте посмотрим, как меняется время полной загрузки страницы в каждом сценарии:
Обнаруженные нами тенденции предыдущих графиков в целом сохраняются и здесь. Я обнаружил, что проталкивание только CSS давало наилучшие показатели времени полной загрузки страницы. Проталкивание слишком большого количества ресурсов может в некоторых случаях сделать веб-сервер немного «вялым», но это было всё же лучше, чем не проталкивать ничего. По сравнению с встраиванием Server Push дала результаты.
Server Push – не панацея от неэффективной работы вашего сайта. Чтобы добиться хороших результатов, эту технологию нужно использовать грамотно. И вот несколько важных моментов.
В одном из приведённых выше сценариев я проталкиваю много ресурсов, но все они составляют небольшую часть общих данных. Проталкивание большого количества очень больших ресурсов могло бы сразу задержать отображение вашей страницы, потому что браузеру нужно загружать не только HTML, но и все другие ресурсы, которые грузятся параллельно. Лучше всего быть избирательным в том, что вы проталкиваете. Таблицы стилей (пока они не очень массивные) – это хороший выбор для начала. Затем оцените, что ещё имеет смысл проталкивать.
Это не всегда плохо, особенно если у вас есть аналитика посетителей. Хорошим примером служит многостраничная форма регистрации, в которой вы проталкиваете ресурсы для следующей страницы, пока идёт процесс регистрации. Однако давайте условимся: если вы не знаете, нужно ли вам принудительно загружать ресурсы для страницы, которую посетители пока не видели, не делайте этого. Некоторые пользователи могут использовать тариф с ограниченным объёмом трафика, и ваша стратегия может стоить им реальных денег.
Некоторые серверы имеют множество параметров конфигурации, связанных с Server Push. Так, у mod_http2
в Apache есть несколько параметров для настройки проталкивания ресурсов. Параметр H2PushPriority
представляет особый интерес, хотя в случае с моим сервером я оставил его по умолчанию. Некоторые эксперименты могут дать дополнительный выигрыш в производительности. На каждом веб-сервере есть целый набор переключателей и настроек, с которыми вы можете поэкспериментировать, поэтому внимательно изучите руководство, чтобы узнать, что вам доступно.
Было много разговоров о том, может ли Server Push стать причиной ухудшения производительности из-за того, что сервер будет повторно проталкивать возвращающимся посетителям ненужные им уже ресурсы. Некоторые серверы делают всё возможное, чтобы смягчить этот момент. В mod_http2
в Apache, например, для этого используется параметр H2PushDiarySize
, а сервер H2O имеет специальную функцию, которая использует механизм cookie для запоминания уже протолкнутых ресурсов.
Если вы не используете H2O-сервер, вы можете добиться того же самого эффекта на своём веб-сервере или в бэкенд-коде, просто инициировав проталкивание при отсутствии файла cookie. Если вам интересно узнать, как это сделать, прочитайте статью, которую я написал об этом на CSS-Tricks. Также стоит помнить о том, что браузеры могут отправлять фрейм RST_STREAM
, чтобы сигнализировать серверу, что проталкиваемый ресурс не нужен. Со временем этот сценарий будет обрабатываться гораздо более изящно.
Уже перенесли свой сайт на HTTP/2? Тогда у вас не так много причин, чтобы не использовать Server Push.
Если у вас очень сложный веб-сайт со множеством ресурсов, начните с малого. Хорошее эмпирическое правило гласит: проталкивайте то, что вам когда-то было удобно встраивать. И подходящей отправной точкой может стать проталкивание CSS вашего сайта. Если после этого вы почувствуете себя более уверенно, подумайте о том, чтобы проталкивать другие вещи. Но всегда проверяйте изменения, чтобы увидеть, как они влияют на производительность.
Если вы не используете для Server Push механизм кеширования, подобный H2O, подумайте о том, как отслеживать пользователей с помощью cookies. Это позволит свести к минимуму ненужные проталкивания для известных пользователей, одновременно повышая производительность для неизвестных. Это хорошо не только хорошо для производительности вашего сайта, но и для пользователей, использующих тарифы с ограниченным объёмом трафика.
Если вы хотите узнать о Server Push больше, советую изучить следующие ресурсы:
От переводчика. Спасибо Yoav Weiss за разъяснение, что атрибут as
обязателен (а не опционален, как указано в оригинальной статье), а также нескольких других незначительных технических проблем. Дополнительная благодарность Джейку Арчибальду за указание, что подсказка ресурса preload
– это оптимизация, отличная от Server Push.
Статья посвящена функции HTTP/2 под названием Server Push. Эта и многие другие темы освещены в книге Джереми Вагнера «Веб-производительность в действии».