Не мешайте работе сканера предварительной загрузки браузера

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

Один из аспектов оптимизации скорости загрузки страниц, о котором часто забывают, – это понимание того, как работают браузеры. Браузеры выполняют определенные оптимизации для повышения производительности способами, которые мы, как разработчики, не можем, но только до тех пор, пока эти оптимизации не будут непреднамеренно сорваны.

Один из внутренних механизмов оптимизации браузера – сканер предварительной загрузки. В этой статье мы расскажем, как работает сканер предварительной загрузки и как не мешать ему.

Что такое сканер предварительной загрузки?

В каждом браузере есть основной синтаксический анализатор HTML, который разбивает на токены необработанную разметку и преобразует ее в объектную модель. Это продолжается до тех пор, пока синтаксический анализатор не обнаружит блокирующий ресурс, например таблицу стилей, загруженную с помощью элемента <link>, или скрипт, загруженный с помощью элемента <script> без атрибута async или defer.

Диаграмма синтаксического анализатора HTML.
Рис. 1. Диаграмма, показывающая, как может быть заблокирован основной синтаксический анализатор HTML в браузере. В этом случае синтаксический анализатор сталкивается с элементом <link> для внешнего файла CSS, который блокирует синтаксический анализ остальной части документа браузером (или даже его рендеринг) до тех пор, пока CSS не будет загружен и проанализирован.

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

Главная страница web.dev в нестилизованном состоянии (слева) и в стилизованном состоянии (справа).
Рис. 2. Пример FOUC. Слева показана главная страница сайта web.dev без стилей. Справа – та же страница с примененными стилями. Если браузер не блокирует отрисовку во время скачивания и обработки таблицы стилей, то страница может на мгновение отобразиться без стилей.

Браузер также блокирует синтаксический анализ и отрисовку страницы, когда обнаруживает элементы <script> без атрибута defer или async.

Это связано с тем, что браузер не может точно знать, изменит ли скрипт DOM, пока основной синтаксический анализатор HTML ещё работает. Поэтому JavaScript обычно загружается в конце документа, чтобы блокировка синтаксического анализа и отрисовки не оказывала существенного влияния.

Это веские причины, по которым браузер должен блокировать как синтаксический анализ, так и отрисовку. Однако блокировать любой из этих важных шагов нежелательно, поскольку они могут задерживать показ, задерживая обнаружение других важных ресурсов. К счастью, браузеры стараются смягчить эти проблемы с помощью вторичного синтаксического анализатора HTML, называемого сканером предварительной загрузки.

Диаграмма основного синтаксического анализатора HTML (слева) и сканера предварительной загрузки (справа), который является вторичным синтаксическим анализатором HTML.
Рис. 3. Схема, показывающая, как сканер предварительной загрузки работает параллельно с основным синтаксическим анализатором HTML для упреждающей загрузки объектов. В этом случае основной синтаксический анализатор HTML заблокирован, поскольку он загружает и обрабатывает CSS, прежде чем сможет начать обработку разметки изображения в элементе <body>. Однако сканер предварительной загрузки может заглянуть в необработанную разметку, чтобы найти этот ресурс изображения и начать его загрузку до того, как основной синтаксический анализатор HTML будет разблокирован.

Сканер предварительной загрузки работает на опережение. Он анализирует необработанную разметку, чтобы найти ресурсы, которые можно получить до того, как их обнаружит основной синтаксический анализатор HTML.

Как узнать, работает ли сканер предварительной загрузки

Сканер предварительной загрузки существует из-за заблокированного рендеринга и синтаксического анализа. Если бы этих двух проблем с производительностью никогда не существовало, сканер предварительной загрузки не был бы очень полезен. Ключ к пониманию того, приносит ли веб-страница пользу от сканера предварительной загрузки, зависит от этих явлений блокировки. Чтобы узнать, где работает сканер предварительной загрузки, можно искусственно задерживать запросы.

В качестве примера рассмотрим эту страницу с простым текстом, изображениями и таблицей стилей. Поскольку файлы CSS блокируют как отрисовку, так и синтаксический анализ, вы вводите искусственную задержку в две секунды для таблицы стилей через прокси-сервис. Эта задержка позволяет легко увидеть в каскаде сети, где работает сканер предварительной загрузки.

На каскадной диаграмме сети WebPageTest показана искусственная задержка в 2 секунды, наложенная на таблицу стилей.
Рис. 4. Каскадная диаграмма сети, созданная с помощью WebPageTest для веб-страницы, которая была открыта в Chrome на мобильном устройстве с имитацией подключения 3G. Несмотря на то, что загрузка таблицы стилей искусственно задерживается прокси-сервером на две секунды, сканер предварительной загрузки обнаруживает изображение, расположенное дальше в разметке.

Как видно на каскадной диаграмме, сканер предварительной загрузки обнаруживает элемент <img>, даже когда рендеринг и синтаксический анализ документа заблокированы. Без этой оптимизации браузер не может получать данные во время блокировки, и запросы ресурсов будут выполняться последовательно, а не параллельно.

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

Внедрено скриптов: async

Предположим, что в разделе <head> вашего HTML-кода есть встроенный код JavaScript, который выглядит следующим образом:

<script>
  const scriptEl = document.createElement('script');
  scriptEl.src = '/yall.min.js';

  document.head.appendChild(scriptEl);
</script>

По умолчанию внедренные скрипты являются async, поэтому при внедрении этого скрипта он будет вести себя так, как если бы к нему был применен атрибут async. Это означает, что он будет выполнен как можно скорее и не заблокирует отрисовку. Звучит оптимально, правда? Однако если вы предполагаете, что этот встроенный элемент <script> идет после элемента <link>, который загружает внешний CSS-файл, вы получите неоптимальный результат:

На этой диаграмме WebPageTest показано, как сканирование предварительной загрузки не выполняется при внедрении скрипта.
Рис. 5. Каскадная диаграмма сети WebPageTest для веб-страницы, запущенной в Chrome на мобильном устройстве с имитацией подключения 3G. Страница содержит одну таблицу стилей и внедренный скрипт async. Сканер предварительной загрузки не может обнаружить скрипт на этапе блокировки отрисовки, поскольку он внедряется на стороне клиента.

Давайте разберем, что произошло:

  1. На нулевой секунде запрашивается основной документ.
  2. На отметке 1, 4 с поступает первый байт запроса навигации.
  3. На второй секунде запрашиваются CSS и изображение.
  4. Поскольку синтаксический анализатор заблокирован загрузкой таблицы стилей, а встроенный код JavaScript, который внедряет скрипт async, появляется после этой таблицы стилей на отметке 2,6 секунды, функциональность, которую предоставляет скрипт, становится доступна позже, чем могла бы.

Это неоптимально, поскольку запрос скрипта происходит только после завершения загрузки таблицы стилей. Это задерживает выполнение скрипта. Элемент <img>, напротив, обнаруживается в разметке, предоставленной сервером, поэтому его находит сканер предварительной загрузки.

Что произойдет, если вы используете обычный тег <script> с атрибутом async, а не вставите скрипт в DOM?

<script src="/yall.min.js" async></script>

Вот что получилось:

Каскадная диаграмма сети WebPageTest, показывающая, как асинхронный скрипт, загруженный с помощью элемента скрипта HTML, по-прежнему обнаруживается сканером предварительной загрузки браузера, даже если основной синтаксический анализатор HTML браузера заблокирован во время загрузки и обработки таблицы стилей.
Рис. 6. Каскадная диаграмма сети WebPageTest для веб-страницы, запущенной в Chrome на мобильном устройстве с имитацией подключения 3G. На странице есть одна таблица стилей и один элемент async <script>. Сканер предварительной загрузки обнаруживает скрипт на этапе блокировки рендеринга и загружает его одновременно с CSS.

Может возникнуть соблазн предположить, что эти проблемы можно решить с помощью rel=preload. Это, конечно, сработает, но может иметь некоторые побочные эффекты. Зачем использовать rel=preload, чтобы устранить проблему, которой можно избежать, если не вставлять элемент <script> в DOM?

Диаграмма WebPageTest, показывающая, как подсказка ресурса rel=preload используется для обнаружения асинхронно внедренного скрипта, хотя и таким образом, что это может иметь непредвиденные побочные эффекты.
Рис. 7. Каскадная диаграмма сети WebPageTest для веб-страницы, запущенной в Chrome на мобильном устройстве с имитацией подключения 3G. На странице есть одна таблица стилей и внедренный скрипт async, но скрипт async предварительно загружен, чтобы его можно было обнаружить раньше.

Предзагрузка "исправляет" проблему здесь, но создает новую проблему: скрипт async в первых двух демонстрациях, несмотря на то, что он загружен в <head>, загружается с "Низким" приоритетом, тогда как таблица стилей загружается с "Наивысшим" приоритетом. В последней демонстрации, где скрипт async предварительно загружен, таблица стилей по-прежнему загружается с наивысшим приоритетом, но приоритет скрипта был повышен до высокого.

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

Ответ прост: если скрипт нужен во время запуска, не мешайте сканеру предварительной загрузки, добавляя его в DOM. Поэкспериментируйте с размещением элемента <script>, а также с атрибутами defer и async.

Отложенная загрузка с помощью JavaScript

Отложенная загрузка – это отличный способ экономии трафика, который часто применяется к изображениям. Однако иногда отложенная загрузка неправильно применяется к изображениям, которые находятся в видимой части страницы.

Это может привести к проблемам с обнаружением ресурсов сканером предварительной загрузки и неоправданно увеличить время, необходимое для обнаружения ссылки на изображение, его скачивания, декодирования и показа. Рассмотрим следующий пример разметки изображения:

<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">

Префикс data- часто используется в загрузчиках на основе JavaScript. Когда изображение попадает в область просмотра, ленивый загрузчик удаляет префикс data-. В приведенном выше примере data-src превращается в src. Это обновление заставляет браузер получить ресурс.

Эта модель не является проблематичной, пока она не применяется к изображениям, которые находятся в области просмотра во время запуска. Поскольку сканер предварительной загрузки не считывает атрибут data-src так же, как атрибут src (или srcset), ссылка на изображение не обнаруживается раньше. Более того, изображение не загрузится, пока не будет скачан, скомпилирован и выполнен код JavaScript, отвечающий за отложенную загрузку.

Каскадная диаграмма загрузки сети WebPageTest, показывающая, как изображение с отложенной загрузкой, находящееся в области просмотра во время запуска, обязательно задерживается, поскольку сканер предзагрузки браузера не может найти ресурс изображения и загружается только тогда, когда загружается JavaScript, необходимый для работы отложенной загрузки. Изображение обнаруживается намного позже, чем должно.
Рис. 8. Каскадная диаграмма сети WebPageTest для веб-страницы, запущенной в Chrome на мобильном устройстве с имитацией подключения 3G. Ресурс изображения загружается с задержкой, хотя он виден в области просмотра при запуске. Это мешает работе сканера предварительной загрузки и приводит к ненужной задержке.

В зависимости от размера изображения, который может зависеть от размера области просмотра, оно может быть элементом-кандидатом для Largest Contentful Paint (LCP). Если сканер предварительной загрузки не может заранее получить ресурс изображения(например, когда таблицы стилей страницы блокируют отрисовку), показатель LCP ухудшается.

Чтобы решить эту проблему, измените разметку изображения:

<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">

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

Каскадная диаграмма сети WebPageTest, на которой показан сценарий загрузки изображения в области просмотра во время запуска. Изображение не загружается отложено, то есть его загрузка не зависит от скрипта, а значит, сканер предварительной загрузки может обнаружить его раньше.
Рис. 9. Каскадная диаграмма сети WebPageTest для веб-страницы, запущенной в Chrome на мобильном устройстве с имитацией подключения 3G. Сканер предварительной загрузки обнаруживает ресурс изображения до того, как начнут загружаться CSS и JavaScript, что дает браузеру преимущество при его загрузке.

В этом упрощенном примере LCP при медленном подключении улучшается на 100 мс. Это может показаться незначительным улучшением, но на самом деле это не так, если учесть, что решение заключается в быстром исправлении разметки и что большинство веб-страниц сложнее, чем этот набор примеров. Это означает, что кандидатам на LCP, возможно, придется конкурировать за пропускную способность со многими другими ресурсами, поэтому подобные оптимизации становятся все более важными.

Фоновые изображения CSS

Помните, что сканер предварительной загрузки браузера сканирует разметку. Другие типы ресурсов, например CSS, которые могут включать получение изображений, на которые ссылается свойство background-image, не сканируются.

Как и в случае с HTML, браузеры обрабатывают CSS в собственную объектную модель, известную как CSSOM. Если при создании CSSOM обнаруживаются внешние ресурсы, они запрашиваются в момент обнаружения, а не сканером предварительной загрузки.

Предположим, что кандидат на LCP вашей страницы – это элемент со свойством CSS background-image. При загрузке ресурсов происходит следующее:

Каскадная диаграмма сети WebPageTest, на которой показана страница с кандидатом на роль LCP, загруженным из CSS с помощью свойства background-image. Поскольку изображение-кандидат LCP относится к типу ресурса, который сканер предварительной загрузки браузера не может проверить, загрузка ресурса задерживается до тех пор, пока CSS не будет загружен и обработан, что задерживает время отрисовки кандидата LCP.
Рис. 10. Каскадная диаграмма WebPageTest, показывающая загрузку веб-страницы в Chrome на мобильном устройстве при имитации подключения 3G. Кандидат на LCP страницы – это элемент со свойством CSS background-image (строка 3). Запрос на получение изображения не отправляется, пока его не обнаружит синтаксический анализатор CSS.

В этом случае сканер предварительной загрузки не столько побежден, сколько не задействован. Тем не менее, если кандидат на роль LCP на странице получен из свойства CSS background-image, вам нужно будет предварительно загрузить это изображение:

<!-- Make sure this is in the <head> below any
     stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">

Подсказка rel=preload небольшая, но она помогает браузеру обнаружить изображение быстрее, чем в противном случае:

Диаграмма сетевых запросов WebPageTest, на которой видно, что фоновое изображение CSS (кандидат на LCP) загружается намного быстрее благодаря использованию подсказки rel=preload. Время отрисовки самого крупного контента уменьшается примерно на 250 миллисекунд.
Рис. 11. Каскадная диаграмма сети WebPageTest для веб-страницы, запущенной в Chrome на мобильном устройстве с имитацией подключения 3G. Кандидат на LCP страницы – это элемент со свойством CSS background-image (строка 3). Подсказка rel=preload помогает браузеру обнаружить изображение примерно на 250 миллисекунд раньше, чем без нее.

Благодаря подсказке rel=preload кандидат на LCP обнаруживается раньше, что сокращает время LCP. Хотя эта подсказка помогает решить проблему, лучше оценить, должно ли изображение, которое может быть самым крупным контентом (LCP), загружаться из CSS. Тег <img> позволяет лучше контролировать загрузку изображения, подходящего для области просмотра, и при этом дает возможность сканеру предварительной загрузки обнаружить его.

Встраивание слишком большого количества ресурсов

Встраивание – это размещение ресурса внутри HTML-кода. Вы можете встраивать таблицы стилей в элементы <style>, скрипты – в элементы <script>, а практически любые другие ресурсы – с помощью кодировки base64.

Встроенные ресурсы могут загружаться быстрее, чем скачиваемые, поскольку для них не отправляется отдельный запрос. Она находится прямо в документе и загружается мгновенно. Однако у этого подхода есть существенные недостатки:

  • Если вы не кешируете HTML-код (а это невозможно, если ответ HTML динамический), встроенные ресурсы никогда не кешируются. Это влияет на производительность, поскольку встроенные ресурсы нельзя использовать повторно.
  • Даже если вы можете кешировать HTML, встроенные ресурсы не будут общими для разных документов. Это снижает эффективность кеширования по сравнению с внешними файлами, которые можно кешировать и повторно использовать во всем источнике.
  • Если встроить слишком много, сканер предварительной загрузки обнаружит ресурсы в документе позже, потому что скачивание дополнительного встроенного контента займет больше времени.

Рассмотрим эту страницу. В некоторых случаях кандидатом на роль LCP является изображение в верхней части страницы, а CSS находится в отдельном файле, загружаемом элементом <link>. На странице также используются четыре веб-шрифта, которые запрашиваются как отдельные файлы из ресурса CSS.

Диаграмма WebPageTest, показывающая загрузку страницы с внешним CSS-файлом, в котором указаны четыре шрифта. Сканер предзагрузки обнаруживает изображение-кандидат LCP в надлежащее время.
Рис. 12. Каскадная диаграмма сети WebPageTest для веб-страницы, запущенной в Chrome на мобильном устройстве с имитацией подключения 3G. Кандидат на LCP страницы – это изображение, загруженное из элемента <img>, но обнаруженное сканером предварительной загрузки, поскольку CSS и шрифты, необходимые для загрузки страницы, загружаются в отдельных ресурсах, что не мешает сканеру предварительной загрузки выполнять свою работу.

Что произойдет, если CSS и все шрифты будут встроены как ресурсы base64?

Диаграмма сетевых запросов WebPageTest для страницы с внешним CSS-файлом, в котором указаны четыре шрифта. Сканер предварительной загрузки обнаруживает изображение LCP со значительной задержкой .
Рис. 13. Каскадная диаграмма сети WebPageTest для веб-страницы, запущенной в Chrome на мобильном устройстве с имитацией подключения 3G. Кандидатом на роль LCP на странице является изображение, загруженное из элемента <img>, но встроенный код CSS и четыре ресурса шрифтов в элементе `` не позволяют сканеру предварительной загрузки обнаружить изображение до тех пор, пока эти ресурсы не будут полностью загружены.

В этом примере встраивание негативно влияет на LCP и на производительность в целом. Версия страницы, на которой ничего не встроено, отрисовывает изображение LCP примерно за 3,5 секунды. На странице, где все ресурсы встроены, изображение LCP отрисовывается только через 7 секунд.

Здесь задействовано больше, чем просто сканер предварительной загрузки. Встраивание шрифтов – не лучшая стратегия, поскольку base64 – неэффективный формат для двоичных ресурсов. Ещё один фактор заключается в том, что внешние ресурсы шрифтов не скачиваются, если они не определены как необходимые в CSSOM. Если эти шрифты встроены как base64, они скачиваются независимо от того, нужны ли они для текущей страницы.

Может ли предварительная загрузка улучшить ситуацию? Хорошо. Вы можете предварительно загрузить изображение LCP и сократить время LCP, но раздувание потенциально некэшируемого HTML-кода встроенными ресурсами имеет другие негативные последствия для производительности. Такой подход также влияет на первую отрисовку контента (FCP). В версии страницы, где ничего не встроено, FCP составляет примерно 2,7 секунды. В версии, где все встроено, FCP составляет примерно 5,8 секунды.

Будьте осторожны со встраиванием контента в HTML, особенно ресурсов, закодированных в base64. Как правило, это не рекомендуется, за исключением очень небольших ресурсов. Встраивайте как можно меньше, потому что слишком много встраивания – это игра с огнем.

Разметка, обрабатываемая с помощью клиентского JavaScript

JavaScript определенно влияет на скорость загрузки страниц. Разработчики используют его не только для обеспечения интерактивности, но и для доставки контента. Это упрощает работу разработчиков, но не всегда приносит пользу пользователям.

Один из шаблонов, который может помешать работе сканера предзагрузки, – это рендеринг разметки с помощью клиентского JavaScript:

Каскадная диаграмма сети WebPageTest, на которой показана базовая страница с изображениями и текстом, полностью отрисованная на клиенте в JavaScript. Поскольку разметка содержится в JavaScript, сканер предварительной загрузки не может обнаружить ни один из ресурсов. Загрузка всех ресурсов дополнительно задерживается из-за того, что фреймворкам JavaScript требуется больше времени на обработку и передачу данных по сети.
Рис. 14. Каскадная диаграмма сети WebPageTest для веб-страницы, отрисованной на стороне клиента, в Chrome на мобильном устройстве с имитацией подключения 3G. Поскольку контент содержится в JavaScript и для его отрисовки используется фреймворк, ресурс изображения в разметке, отрисованной на стороне клиента, скрыт от сканера предварительной загрузки. На рис. 9 показан эквивалентный вариант, обработанный на сервере.

Если полезная нагрузка разметки содержится в JavaScript и полностью обрабатывается им в браузере, то сканер предварительной загрузки не может обнаружить ресурсы в этой разметке. Это задерживает обнаружение важных ресурсов, что, безусловно, влияет на LCP. В этих примерах запрос изображения LCP значительно задерживается по сравнению с эквивалентным рендерингом на стороне сервера, для которого не требуется JavaScript.

Это немного выходит за рамки данной статьи, но последствия рендеринга разметки на клиенте выходят далеко за рамки победы над сканером предварительной загрузки. Во-первых, использование JavaScript для реализации функций, которые не требуют этого, увеличивает время обработки, что может повлиять на показатель Interaction to Next Paint (INP). Отрисовка большого объема разметки на стороне клиента с большей вероятностью приведет к длительным задачам, чем если бы этот же объем разметки отправлялся сервером. Это связано не только с дополнительной обработкой, которую требует JavaScript, но и с тем, что браузеры передают разметку с сервера и разбивают рендеринг на части, что позволяет ограничить длительные задачи. Разметка, отрисованная на стороне клиента, обрабатывается как одна монолитная задача, что может повлиять на INP страницы.

Решение этой проблемы зависит от ответа на следующий вопрос: есть ли причина, по которой разметка страницы не может быть предоставлена сервером, а не отрисована на клиенте? Если нет, то по возможности следует использовать отрисовку на стороне сервера или статически сгенерированную разметку. Это поможет сканеру предварительной загрузки обнаруживать и заранее получать важные ресурсы.

Если для работы некоторых элементов разметки на странице требуется JavaScript, вы можете использовать SSR в сочетании с чистым JavaScript или гидратацией.

Как помочь сканеру предварительной загрузки

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

Вот основные выводы из этой статьи:

  • Сканер предварительной загрузки браузера – это вторичный синтаксический анализатор HTML, который сканирует код раньше основного, если тот заблокирован, чтобы обнаружить ресурсы, которые можно получить раньше.
  • Ресурсы, которых нет в разметке, предоставленной сервером по первому запросу навигации, не могут быть обнаружены сканером предварительной загрузки. Сканер предварительной загрузки может не сработать в следующих случаях:
    • Внедрение ресурсов в DOM с помощью JavaScript, будь то скрипты, изображения, таблицы стилей или что-либо ещё, что лучше было бы разместить в исходной полезной нагрузке разметки с сервера.
    • Отложенная загрузка изображений или элементов iframe в верхней части страницы с помощью JavaScript.
    • Отрисовка разметки на клиенте, которая может содержать ссылки на дополнительные ресурсы документа, с помощью JavaScript.
  • Сканер предварительной загрузки сканирует только HTML. Он не проверяет содержимое других ресурсов, в частности CSS, которые могут содержать ссылки на важные объекты, включая кандидатов на LCP.

Если по какой-либо причине вы не можете избежать шаблона, который негативно влияет на способность сканера предварительной загрузки ускорять загрузку, используйте подсказку ресурса rel=preload. Если вы используете rel=preload, проверьте в лабораторных инструментах, дает ли он желаемый эффект. Наконец, не стоит предварительно загружать слишком много ресурсов, потому что если вы сделаете приоритетным всё, то приоритетным не будет ничего.

Ресурсы