javascript

Исчерпывающее руководство по использованию HTTP/2 Server Push

  • вторник, 30 мая 2017 г. в 03:13:49
https://habrahabr.ru/company/badoo/blog/329722/
  • Разработка веб-сайтов
  • Высокая производительность
  • JavaScript
  • HTML
  • Блог компании Badoo



Привет! Меня зовут Александр, и я – фронтенд-разработчик в компании Badoo. Пожалуй, одной из самых обсуждаемых тем в мире фронтенда в последние несколько лет является протокол HTTP/2. И не зря – ведь переход на него открывает перед разработчиками много возможностей по ускорению и оптимизации сайтов. Этот пост посвящён как раз одной из таких возможностей – Server Push. Cтатья Джереми Вагнера показалась мне интересной, и поэтому делюсь полезной информацией с вами.


Не так давно возможности разработчиков, ориентированных на производительность, заметно изменились. И появление HTTP/2 стало, возможно, самым значительным изменением. HTTP/2 больше не является фичей, которую мы с нетерпением ждём, — он уже существует (и успешно помогает справляться с проблемами вроде блокировки начала очереди и несжатых заголовков, существующими в HTTP/1), а «в комплекте» с ним идёт Server Push!


Эта технология позволяет отправлять пользователям ресурсы сайта, прежде чем они их попросят. Это элегантный способ добиться преимущества в производительности методов оптимизации HTTP/1, таких как, например, встраивание, и избежать недостатков, связанных с этой практикой.


Из этой статьи вы узнаете всё о Server Push – от принципа её работы до решаемых ею проблем: как её использовать, как определить, работает ли она и каково её влияние на производительность, и многое другое.


Что такое 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, но далеко не единственная.


Так, 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?


Использование 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);
  • функцией бекенд-языка (например, PHP-функция header).


Вот пример настройки сервера 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 – использование серверного языка. Это поможет, если вы не можете изменить настройки веб-сервера. Ниже приведён пример использования 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>

Этот синтаксис более удобен, чем объединение нескольких значений, разделённых запятыми, а работает не хуже. Единственным минусом является его недостаточная компактность, но удобство стоит нескольких лишних байтов, передаваемых по сети.


Теперь, когда вы знаете, как проталкивать ресурсы, давайте посмотрим, как определить, работает ли это.


Как определить, работает ли Server Push?


Итак, вы добавили заголовок 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 влияет на производительность реального веб-сайта.


Замер производительности Server Push


Измерение эффекта любого повышения производительности требует хорошего инструмента тестирования. Sitespeed.io – отличный инструмент, доступный через npm; он автоматизирует тестирование страниц и собирает ценные показатели производительности.


Итак, мы выбрали подходящий инструмент – переходим к методологии тестирования.


Методология тестирования


Я хотел измерить влияние Server Push на производительность веб-сайта. Чтобы результаты были релевантными, мне нужно было установить точки сравнения по шести отдельным сценариям. Эти сценарии разделены на два аспекта: используется HTTP/2 или HTTP/1. На серверах HTTP/2 мы измеряем влияние Server Push по ряду показателей; на серверах HTTP/1 – хотим увидеть, как встраивание ресурсов влияет на производительность по тем же показателям, поскольку встраивание должно обладать примерно теми же преимуществами, которые обеспечивает Server Push.


Рассматриваемые сценарии:


  • HTTP/2 – No Enhancements
    В этом состоянии сайт работает по протоколу HTTP/2, но абсолютно ничего не проталкивается.
    • HTTP/2 – Push CSS
      Server Push используется, но только для CSS сайта. CSS сайта совсем небольшой, весом чуть более 2 КБ, сжатый с использованием алгоритма Brotli.
    • HTTP/2 – Push Everything
      Все ресурсы, которые используются на страницах сайта, проталкиваются. Это включает в себя CSS, а также 1,4 КБ JavaScript-кода, разбросанных по шести файлам, и 5,9 КБ SVG-изображений, разбросанных по пяти файлам. Все размеры файлов указаны после сжатия (так же с применением Brotli).
    • HTTP/1 – No Enhancements
      Веб-сайт работает по протоколу HTTP/1, и никакие ресурсы для уменьшения количества запросов или увеличения скорости рендеринга не встраивались.
    • HTTP/1 – Inline CSS
      Все CSS сайта встроены.
    • HTTP/1 – Inline Everything
      Все ресурсы, используемые на страницах веб-сайта, являются встроенными. CSS и сценарии встроены, а SVG-изображения имеют кодировку base64 и встроены непосредственно в разметку. Следует отметить, что данные, закодированные в формате base64, примерно в 1,37 раза больше, чем их некодированные эквиваленты.

Для каждого сценарии я инициировал тестирование с помощью следующей команды:


sitespeed.io -d 1 -m 1 -n 25 -c cable -b chrome –v
https://jeremywagner.me

Если вы хотите узнать, что делает эта команда, вы можете посмотреть документацию. Если вкратце, она проверяет домашнюю страницу моего сайта по адресу https://jeremywagner.me со следующими условиями:


  • ссылки на странице не сканируются, тестируется только указанная страница;
    страница тестируется 25 раз;
  • используется сетевой профиль, соответствующий 28 миллисекундам времени прохождения сигнала туда и обратно, входящая скорость – 5000 кбит/с, исходящая – 1000 кбит/с;
  • тест запускается с помощью Google Chrome.

Для каждого теста были собраны и отображены три показателя:


  • First Paint Time
    Это момент времени, когда страница начала отображаться в браузере. Чтобы казалось, что страница загружается быстро, этот показатель нужно уменьшать как можно больше.


  • DOMContentLoaded Time
    Это время, когда HTML документ был полностью загружен и разобран. Синхронный JavaScript-код блокирует парсер и увеличивает этот показатель. Использование атрибута async в тегах <script> может помочь предотвратить блокировку парсера.


  • Page Load Time
    Это время, необходимое для полной загрузки страницы и её ресурсов.

Определив параметры теста, давайте посмотрим на результаты.


Результаты тестирования


Тесты проводились по шести сценариям, указанным выше, с построением графиков результатов. Давайте начнём с рассмотрения того, как каждый сценарий влияет на время начала отображения страницы:



Сначала несколько слов о том, как настроен график. Часть графика в синем цвете соответствует среднему времени. Оранжевая часть – это уровень 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


Server Push – не панацея от неэффективной работы вашего сайта. Чтобы добиться хороших результатов, эту технологию нужно использовать грамотно. И вот несколько важных моментов.


Вы можете протолкнуть слишком много ресурсов


В одном из приведённых выше сценариев я проталкиваю много ресурсов, но все они составляют небольшую часть общих данных. Проталкивание большого количества очень больших ресурсов могло бы сразу задержать отображение вашей страницы, потому что браузеру нужно загружать не только HTML, но и все другие ресурсы, которые грузятся параллельно. Лучше всего быть избирательным в том, что вы проталкиваете. Таблицы стилей (пока они не очень массивные) – это хороший выбор для начала. Затем оцените, что ещё имеет смысл проталкивать.


Вы можете протолкнуть ресурсы, не относящиеся к текущей странице


Это не всегда плохо, особенно если у вас есть аналитика посетителей. Хорошим примером служит многостраничная форма регистрации, в которой вы проталкиваете ресурсы для следующей страницы, пока идёт процесс регистрации. Однако давайте условимся: если вы не знаете, нужно ли вам принудительно загружать ресурсы для страницы, которую посетители пока не видели, не делайте этого. Некоторые пользователи могут использовать тариф с ограниченным объёмом трафика, и ваша стратегия может стоить им реальных денег.


Настройте ваш HTTP/2-сервер правильно


Некоторые серверы имеют множество параметров конфигурации, связанных с 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. Эта и многие другие темы освещены в книге Джереми Вагнера «Веб-производительность в действии».