HTTP/2 обещал заметно ускорить веб, и Cloudflare давным-давно развернула доступ по HTTP/2 для всех клиентов. Но одна особенность HTTP/2, приоритизация, не соответствовала ожиданиям. Не потому, что она принципиально сломана, а из-за реализации в браузерах.
Сегодня Cloudflare предлагает изменить приоритизацию HTTP/2, что даёт нашим серверам контроль над решениями о приоритизации, которые действительно заметно ускоряют интернет.
Исторически именно браузер контролирует, как и когда загружать веб-контент. Сегодня для всех платных планов мы вносим радикальные изменения в эту модель. Они передают контроль напрямую владельцу сайта. На вкладке «Скорость» в панели мониторинга Cloudflare клиенты могут включить «Расширенную приоритизацию HTTP/2»: она переопределяет настройки браузера по умолчанию на улучшенную схему планирования, что значительно ускоряет доступ для посетителей (в некоторых случаях мы видели ускорение на 50%). С воркерами Cloudflare владельцы сайтов могут пойти ещё дальше и полностью подобрать настройки под свои конкретные нужды.
Нынешняя ситуация
Веб-страницы состоят из десятков (иногда сотен) отдельных ресурсов, которые загружаются и собираются браузером в конечный отображаемый контент. Это включает в себя видимый контент, с которым взаимодействует пользователь (HTML, CSS, изображения), а также логику приложения (JavaScript) для самого сайта, рекламу, аналитику и маркетинговые следящие маячки. С точки зрения пользователя очень важна последовательность, в которой загружаются эти ресурсы: это влияет на время, когда он увидит содержимое и сможет взаимодействовать со страницей.
Браузер — это, по сути, движок обработки HTML, который проходит через HTML-документ и следует инструкциям по порядку: от начала до конца HTML, выстраивая страницу по мере продвижения. Ссылки на таблицы стилей (CSS) сообщают браузеру, как стилизовать содержимое страницы, и браузер задержит отображение контента до тех пор, пока не загрузит таблицу стилей. У скриптов на странице могут быть разные варианты поведения. Если скрипт помечен как «асинхронный» или «отложенный», браузер может продолжать обработку документа и просто запустить скрипт, когда он станет доступен. Если скрипт не помечен как асинхронный или отложенный, браузер ДОЛЖЕН прекратить обработку документа до тех пор, пока скрипт не загрузится и не выполнится. Такие скрипты называются «блокирующими», поскольку они блокируют браузеру возможность продолжать обработку документа.
HTML-документ делится на две части. Заголовок документа <head> находится в начале и содержит таблицы стилей, скрипты и другие инструкции для браузера, необходимые для отображения содержимого. После заголовка идёт тело документа <body>, оно содержит фактический контент, отображаемый в окне браузера (хотя скрипты и таблицы стилей также могут быть в теле). Пока браузер не доберётся до тела документа, пользователю нечего показывать, и страница останется пустой. Поэтому важно как можно быстрее обработать заголовок. Если вам интересны подробности, на сайте HTML5 Rocks есть отличный учебник, как работают браузеры.
Браузер обычно отвечает за порядок загрузки различных ресурсов, необходимых для построения страницы и дальнейшей обработки документа. В HTTP/1.x действуют ограничения, сколько объектов браузер может запросить с любого сервера за один раз (обычно 6 подключений и только один ресурс за раз на соединение), поэтому порядок запросов строго контролируется браузером. В HTTP/2 ситуация совершенно иная. Браузер может запросить сразу все ресурсы (по крайней мере, как только узнает о них), и предоставляет серверу подробные инструкции, как доставлять эти ресурсы.
Оптимальный порядок загрузки ресурсов
Для большинства частей в цикле загрузки страницы есть оптимальный порядок, который максимально ускоряет доступность страницы для пользователя (а разница между оптимальным и неоптимальным порядком загрузки может достигать 50% и более).
Как описано выше, прежде чем браузер сможет отобразить какой-либо контент, его блокируют CSS и JavaScript в разделе <head>
. На этом этапе выгоднее задействовать 100% канала для загрузки блокирующих ресурсов, а не загружать их по порядку, как они прописаны в HTML-коде. Это позволяет браузеру анализировать и запускать каждый элемент во время загрузки следующего блокирующего ресурса, что создаёт оптимальный конвейер.
Время загрузки скриптов при параллельной или последовательной загрузке не отличается, но при последовательной загрузке первый скрипт можно обработать и выполнить во время загрузки второго.
После загрузки блокирующих ресурсов ситуация становится немного интереснее. Здесь оптимальная загрузка может зависеть от конкретного сайта или даже бизнес-приоритетов (выбор пользовательского контента или рекламы, или аналитики и т. д.). Отдельная проблема со шрифтами, поскольку браузер обнаруживает нужные шрифты после применения таблицы стилей к отображаемому контенту. Поэтому к моменту, когда браузер узнает о шрифте, необходимо отобразить текст, который уже готов к выводу на экран. Любые задержки в загрузке шрифта приводят к отсутствию текста на экране (или текст отображается неправильным шрифтом).
Как правило, необходимо учитывать некоторые компромиссы:
- Пользовательские шрифты и изображения в видимой части страницы (viewport) следует загрузить как можно быстрее. Они напрямую влияют на визуальный опыт пользователя при загрузке страницы.
- Неблокирующий JavaScript следует загружать последовательно относительно других ресурсов JavaScript, чтобы выполнение каждого из них можно было поставить в конвейер. JavaScript может включать пользовательскую логику приложений, а также маячки отслеживания для аналитики и маркетинга, а их задержка может привести к снижению показателей, отслеживаемых бизнесом.
- Изображения можно загружать параллельно. Первые несколько байт файла изображения содержат его размеры, что может понадобиться для макета браузера, а параллельная загрузка прогрессивных изображений может обеспечить визуальную завершённость после передачи примерно всего 50% общего объёма.
С учётом компромиссов, в большинстве случаев хорошо работает такая стратегия:
- Пользовательские шрифты загружаются последовательно и делят доступную полосу пропускания с изображениями в области видимости.
- Видимые изображения загружаются параллельно, разделяя между собой часть полосы пропускания, выделенную на них.
- Когда больше нет шрифтов или видимых изображений:
- Неблокирующие скрипты загружаются последовательно и разделяют доступную полосу пропускания с невидимыми изображениями (которые находятся вне области видимости).
- Невидимые изображения загружаются параллельно, разделяя между собой часть полосы пропускания, выделенную на них.
Таким образом, видимый пользователю контент загружается как можно быстрее, логика приложения задерживается по минимуму, а невидимые изображения загружаются таким образом, чтобы как можно быстрее завершить макет.
Пример
Для иллюстрации используем упрощённую страницу категории продукта с типичного сайта электронной коммерции:
- Синий — HTML-файл самой страницы.
- Зелёный — Одна внешняя таблица стилей (CSS-файл).
- Оранжевый — Четыре внешних скрипта (JavaScript). Два блокирующих скрипта в начале страницы и два асинхронных. Блокирующие скрипты показаны более тёмным оттенком оранжевого.
- Красный — один пользовательский веб-шрифт.
- Фиолетовый — 13 изображений. В окне просмотра отображаются логотип страницы и четыре изображения продукта, ещё 8 изображений продукта требуют прокрутки. Пять видимых изображениях обозначены более тёмным оттенком фиолетового.
Для простоты предположим, что у всех ресурсов одинаковый размер и каждый загружается за 1 секунду. Загрузка всех ресурсов занимает в общей сложности 20 секунд, но крайне важны порядок и метод загрузки.
Вот как будет выглядеть в браузере оптимальная загрузка ресурсов:
- Страница пуста в течение первых 4 секунд, пока загружаются HTML, CSS и блокирующие скрипты: все они используют 100% соединения.
- На 4-секундной отметке фон и структура страницы отображаются без текста или изображений.
- Через секунду, на отметке в 5 секунд, отображается текст страницы.
- В промежутке 5−10 секунд загружаются изображения, сначала размытые, но очень быстро они становятся чёткими. Примерно на 7 секунде результат почти неотличим от окончательной версии.
- На отметке 10 секунд завершена загрузка всего визуального содержимого в видимой части страницы.
- В течение следующих двух секунд загружается и выполняется асинхронный JavaScript, выполняя любую некритическую логику (аналитика, маркетинговые теги и т. д.).
- В течение последних 8 секунд загружаются остальные изображения на случай прокрутки страницы пользователем.
Текущая приоритизация в браузерах
Все текущие браузерные движки реализуют различные стратегии приоритизации, ни одна из которых не является оптимальной.
Microsoft Edge и Internet Explorer не поддерживают приоритизацию, поэтому работают с настройками HTTP/2 по умолчанию, который всё загружает параллельно, равномерно распределяя пропускную способность между всеми ресурсами. Microsoft Edge в будущих версиях переходит к использованию движка Chromium, что может улучшить ситуацию. Но пока в нашем примере браузер большую часть времени застрянет в заголовке страницы, так как изображения замедляют передачу блокирующих скриптов и таблиц стилей.
Визуально это приводит к довольно болезненному опыту: пользователь в течение 19 секунд смотрит на пустой экран, а затем происходит задержка на 1 секунду для отображения текста. При просмотре анимации внизу будьте терпеливы, потому что в течение 19 секунд может показаться, что на пустом экране ничего не происходит (хотя так и есть):
Safari загружает все ресурсы параллельно, разделяя пропускную способность на основе их важности, по мнению Safari (блокирующие ресурсы, такие как скрипты и таблицы стилей, важнее, чем изображения). Изображения загружаются параллельно, но также одновременно с блокирующим контентом.
Хотя Safari похож на Edge в том смысле, что всё загружается одновременно, но выделение большей полосы для блокирующих ресурсов позволяет отобразить контент намного раньше:
- Примерно через 8 секунд завершается загрузка таблицы стилей и скриптов, поэтому можно начинать рендеринг страницы. Поскольку изображения загружались параллельно, их тоже можно отобразить частично (размыто для прогрессивных изображений). Это всё ещё в два раза медленнее оптимального сценария, но намного лучше, чем в Edge.
- Примерно через 11 секунд загружается шрифт. Можно отобразить текст. К этому моменту загружается больше данных для изображений, и они становятся немного резче. Это сопоставимо с ситуацией в районе 7-секундной отметки для оптимального сценария загрузки.
- В течение оставшихся 9 секунд изображения становятся более чёткими, поскольку загружается больше данных, пока, наконец, процесс не завершается за 20 секунд.
Firefox создаёт дерево зависимостей, которое группирует ресурсы, а затем планирует группы либо загружать одну за другой, либо совместно разделить пропускную способность между группами. В пределах данной группы ресурсы совместно используют пропускную способность и загружаются одновременно. Изображения планируется загружать после таблиц стилей, блокирующих рендеринг, и загружать параллельно, но сценарии и таблицы стилей, блокирующие рендеринг, также загружаются параллельно и не получают преимуществ конвейерной обработки.
В нашем примере это происходит немного быстрее, чем в Safari, так как изображения ждут завершения загрузки таблиц стилей:
- На отметке 6 секунд исходный контент страницы отображается с фоном и размытыми версиями изображений продукта (по сравнению с 8 секундами у Safari и 4 секундами в оптимальном случае).
- На 8 секунде загрузился шрифт, и можно отобразить текст вместе с немного более чёткими изображениями продукта (по сравнению с 11 секундами у Safari и 7 секундами в оптимальном случае).
- В течение оставшихся 12 секунд изображения становятся более чёткими по мере загрузки оставшегося содержимого.
Chrome (и все браузеры на основе Chromium) приоритизирует ресурсы по списку. Это очень хорошо работает для блокирующих ресурсов, которые оптимально загружать по порядку, но не так хорошо для изображений. Каждое изображение загружается до 100% перед началом загрузки следующего.
На практике это почти оптимальный сценарий загрузки, с той лишь разницей, что изображения загружаются по одному, а не параллельно:
- До отметки 5 секунд загрузка Chrome идентична оптимальному сценарию, отображая фон на 4-й секунде и текстовое содержимое на 5-й.
- В течение следующих 5 секунд изображения области видимости загружаются по одному, пока процесс не завершится на отметке 10 секунд (по сравнению с оптимальным сценарием, когда они отображаются в немного размытом виде на отметке 7 секунд и становятся более чёткими в течение оставшихся трёх секунд).
- После завершения визуальной части страницы за 10 секунд (идентично оптимальному сценарию), оставшиеся 10 секунд тратятся на запуск асинхронных скриптов и загрузку скрытых изображений (так же, как и в оптимальном сценарии).
Визуальное сравнение
Визуальная разница довольно сильно отличается, хотя технически загрузка всего контента занимает одинаковое время:
Приоритизация на стороне сервера
Приоритизация HTTP/2 запрашивается клиентом (браузером), и сервер должен решить, что делать на основе запроса. Большое количество серверов не вообще не поддерживают эту функцию, а остальные выполняют запрос клиента. Другой вариант — принять решение о наилучшей приоритизации на стороне сервера с учётом запроса клиента.
Согласно спецификации, приоритизация HTTP/2 — это дерево зависимостей, которое требует полного знания всех текущих запросов, чтобы иметь возможность приоритизировать ресурсы друг относительно друга. Это позволяет реализовать невероятно сложные стратегии, но такое трудно хорошо реализовать на стороне браузера или сервера (о чём свидетельствуют различные стратегии браузера и различные уровни поддержки сервера). Чтобы упростить управление приоритизацией, мы разработали более простую схему, которая по-прежнему обладает всей гибкостью, необходимой для оптимального планирования.
Схема приоритизации Cloudflare состоит из 64 приоритетных «уровней», а внутри каждого уровня есть группы ресурсов, которые определяют, как разделить между собой соединение:
Сначала скачиваются все ресурсы на более высоком уровне приоритета, затем происходит переход на более низкий уровень.
В пределах заданного уровня приоритета существует три различных группы параллелизма (concurrency):
- 0: все ресурсы в группе “0” отправляются последовательно в том порядке, в котором они были запрошены, используя 100% пропускной способности. Только после загрузки всех ресурсов группы “0” рассматриваются другие группы на том же уровне.
- 1: все ресурсы в группе параллелизма “1” отправляются последовательно в том порядке, в котором были запрошены. Доступная полоса пропускания равномерно распределяется между группой параллелизма “1” и группой параллелизма “n”.
- n: ресурсы в группе параллелизма “n” передаются параллельно, разделяя между собой доступную пропускную способность.
На практике группа параллелизма “0” полезна для критического контента, который необходимо обрабатывать последовательно (скрипты, CSS и т. д.). Группа “1” полезна для менее важного контента, который может совместно использовать пропускную способность с другими ресурсами, но где сами ресурсы по-прежнему выигрывают от последовательной обработки (асинхронные скрипты, непрогрессивные изображения и т. д.). Группа параллелизма “n” полезна для ресурсов, которые выигрывают от параллельной обработки (прогрессивные изображения, видео, аудио и т. д.).
Приоритизация по умолчанию в Cloudflare
При опции расширенной приоритизации реализуется «оптимальный» порядок загрузки ресурсов, описанный выше. Применяемые конкретные приоритеты выглядят следующим образом:
Эта схема позволяет последовательно отправлять ресурсы, блокирующие рендеринг, затем параллельно отправлять видимые изображения, а потом — остальную часть содержимого страницы с некоторым уровнем совместного использования полосы для балансировки загрузки приложения и содержимого. Предостережение * If Detectable заключается в том, что не все браузеры различают различные типы таблиц стилей и скриптов, но всё равно это будет значительно быстрее во всех случаях. Ускорение на 50%, особенно для посетителей Edge и Safari, не станет чем-то необычным:
Настройка приоритизации с воркерами
Более быстрая работа по умолчанию — это отлично, но всё становится действительно интересным благодаря возможности настройки приоритизации с поддержкой Cloudflare Workers, так что сайты могут переопределить приоритет по умолчанию для ресурсов или реализовать свои собственные схемы приоритизации.
Если воркер добавляет к ответу заголовок cf-priority
, то граничные серверы Cloudflare применят указанный приоритет и параллелизм. Формат заголовка — <priority>/<concurrency>, поэтому заголовок response.headers.set('cf-priority', “30/0”);
установит для данного ответа приоритет 30 и параллелизм 0. Аналогично, “30/1” установит параллелизм “1”, а “30/n” установит параллелизм на n.
С такой гибкостью сайт может настроить произвольный приоритет ресурсов для своих потребностей. Например, повысить приоритет некоторых важных асинхронных скриптов или главных изображений: они скачаются ещё до того, как браузер определил, что они находятся в зоне видимости.
Для информирования о решениях приоритизации рантайм воркеров также указывает запрошенную браузером информацию о приоритизации в объекте запроса, который передаётся приёмнику событий воркера (request.cf.requestPriority). Входящие приоритеты представляют собой список атрибутов, разделённых точкой с запятой. Он выглядит примерно так: weight=192;exclusive=0;group=3;group-weight=127
.
- weight: вес для приоритизации HTTP/2.
- exclusive: эксклюзивный флаг HTTP/2 (1 для браузеров на основе Chromium, 0 для других).
- group: идентификатор потока HTTP/2 для группы запросов (ненулевой для Firefox).
- group-weight: вес HTTP/2 для группы запросов (ненулевой для Firefox).
Это только начало
Способность настраивать и контролировать приоритетность ответов является основным строительным блоком для большой будущей работы. Мы намерены внедрять собственные передовые оптимизации поверх этой, но с поддержкой воркеров все сайты и исследователи могут экспериментировать с различными стратегиями приоритизации. Через Apps Marketplace компании также могут создавать новые сервисы оптимизации поверх рабочей платформы и предоставлять их для использования другим сайтам.
Если вы находитесь на плане Pro или выше, перейдите на вкладку «Скорость» в панели мониторинга Cloudflare и включите «расширенную приоритизацию HTTP/2» для ускорения своего сайта.
Автор: m1rko