Узнайте, что такое сканер предварительной загрузки браузера, как он помогает повысить производительность и как не мешать ему.
Один из аспектов оптимизации скорости загрузки страниц, о котором часто забывают, – это понимание того, как работают браузеры. Браузеры выполняют определенные оптимизации для повышения производительности способами, которые мы, как разработчики, не можем, но только до тех пор, пока эти оптимизации не будут непреднамеренно сорваны.
Один из внутренних механизмов оптимизации браузера – сканер предварительной загрузки. В этой статье мы расскажем, как работает сканер предварительной загрузки и как не мешать ему.
Что такое сканер предварительной загрузки?
В каждом браузере есть основной синтаксический анализатор HTML, который разбивает на токены необработанную разметку и преобразует ее в объектную модель. Это продолжается до тех пор, пока синтаксический анализатор не обнаружит блокирующий ресурс, например таблицу стилей, загруженную с помощью элемента <link>, или скрипт, загруженный с помощью элемента <script> без атрибута async или defer.
<link> для внешнего файла CSS, который блокирует синтаксический анализ остальной части документа браузером (или даже его рендеринг) до тех пор, пока CSS не будет загружен и проанализирован.
В случае с файлами CSS отрисовка блокируется, чтобы предотвратить кратковременное появление нестилизованного контента, когда версия страницы без стилей может быть видна в течение короткого времени до того, как к ней будут применены стили.
Браузер также блокирует синтаксический анализ и отрисовку страницы, когда обнаруживает элементы <script> без атрибута defer или async.
Это связано с тем, что браузер не может точно знать, изменит ли скрипт DOM, пока основной синтаксический анализатор HTML ещё работает. Поэтому JavaScript обычно загружается в конце документа, чтобы блокировка синтаксического анализа и отрисовки не оказывала существенного влияния.
Это веские причины, по которым браузер должен блокировать как синтаксический анализ, так и отрисовку. Однако блокировать любой из этих важных шагов нежелательно, поскольку они могут задерживать показ, задерживая обнаружение других важных ресурсов. К счастью, браузеры стараются смягчить эти проблемы с помощью вторичного синтаксического анализатора HTML, называемого сканером предварительной загрузки.
<body>. Однако сканер предварительной загрузки может заглянуть в необработанную разметку, чтобы найти этот ресурс изображения и начать его загрузку до того, как основной синтаксический анализатор HTML будет разблокирован.
Сканер предварительной загрузки работает на опережение. Он анализирует необработанную разметку, чтобы найти ресурсы, которые можно получить до того, как их обнаружит основной синтаксический анализатор HTML.
Как узнать, работает ли сканер предварительной загрузки
Сканер предварительной загрузки существует из-за заблокированного рендеринга и синтаксического анализа. Если бы этих двух проблем с производительностью никогда не существовало, сканер предварительной загрузки не был бы очень полезен. Ключ к пониманию того, приносит ли веб-страница пользу от сканера предварительной загрузки, зависит от этих явлений блокировки. Чтобы узнать, где работает сканер предварительной загрузки, можно искусственно задерживать запросы.
В качестве примера рассмотрим эту страницу с простым текстом, изображениями и таблицей стилей. Поскольку файлы CSS блокируют как отрисовку, так и синтаксический анализ, вы вводите искусственную задержку в две секунды для таблицы стилей через прокси-сервис. Эта задержка позволяет легко увидеть в каскаде сети, где работает сканер предварительной загрузки.
Как видно на каскадной диаграмме, сканер предварительной загрузки обнаруживает элемент <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-файл, вы получите неоптимальный результат:
async. Сканер предварительной загрузки не может обнаружить скрипт на этапе блокировки отрисовки, поскольку он внедряется на стороне клиента.
Давайте разберем, что произошло:
- На нулевой секунде запрашивается основной документ.
- На отметке 1, 4 с поступает первый байт запроса навигации.
- На второй секунде запрашиваются CSS и изображение.
- Поскольку синтаксический анализатор заблокирован загрузкой таблицы стилей, а встроенный код JavaScript, который внедряет скрипт
async, появляется после этой таблицы стилей на отметке 2,6 секунды, функциональность, которую предоставляет скрипт, становится доступна позже, чем могла бы.
Это неоптимально, поскольку запрос скрипта происходит только после завершения загрузки таблицы стилей. Это задерживает выполнение скрипта. Элемент <img>, напротив, обнаруживается в разметке, предоставленной сервером, поэтому его находит сканер предварительной загрузки.
Что произойдет, если вы используете обычный тег <script> с атрибутом async, а не вставите скрипт в DOM?
<script src="/yall.min.js" async></script>
Вот что получилось:
async <script>. Сканер предварительной загрузки обнаруживает скрипт на этапе блокировки рендеринга и загружает его одновременно с CSS.
Может возникнуть соблазн предположить, что эти проблемы можно решить с помощью rel=preload. Это, конечно, сработает, но может иметь некоторые побочные эффекты. Зачем использовать rel=preload, чтобы устранить проблему, которой можно избежать, если не вставлять элемент <script> в DOM?
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, отвечающий за отложенную загрузку.
В зависимости от размера изображения, который может зависеть от размера области просмотра, оно может быть элементом-кандидатом для Largest Contentful Paint (LCP). Если сканер предварительной загрузки не может заранее получить ресурс изображения(например, когда таблицы стилей страницы блокируют отрисовку), показатель LCP ухудшается.
Чтобы решить эту проблему, измените разметку изображения:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Это оптимальный шаблон для изображений, которые находятся в области просмотра во время запуска, поскольку сканер предварительной загрузки быстрее обнаружит и получит ресурс изображения.
В этом упрощенном примере LCP при медленном подключении улучшается на 100 мс. Это может показаться незначительным улучшением, но на самом деле это не так, если учесть, что решение заключается в быстром исправлении разметки и что большинство веб-страниц сложнее, чем этот набор примеров. Это означает, что кандидатам на LCP, возможно, придется конкурировать за пропускную способность со многими другими ресурсами, поэтому подобные оптимизации становятся все более важными.
Фоновые изображения CSS
Помните, что сканер предварительной загрузки браузера сканирует разметку. Другие типы ресурсов, например CSS, которые могут включать получение изображений, на которые ссылается свойство background-image, не сканируются.
Как и в случае с HTML, браузеры обрабатывают CSS в собственную объектную модель, известную как CSSOM. Если при создании CSSOM обнаруживаются внешние ресурсы, они запрашиваются в момент обнаружения, а не сканером предварительной загрузки.
Предположим, что кандидат на LCP вашей страницы – это элемент со свойством CSS background-image. При загрузке ресурсов происходит следующее:
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 небольшая, но она помогает браузеру обнаружить изображение быстрее, чем в противном случае:
background-image (строка 3). Подсказка rel=preload помогает браузеру обнаружить изображение примерно на 250 миллисекунд раньше, чем без нее.
Благодаря подсказке rel=preload кандидат на LCP обнаруживается раньше, что сокращает время LCP. Хотя эта подсказка помогает решить проблему, лучше оценить, должно ли изображение, которое может быть самым крупным контентом (LCP), загружаться из CSS. Тег <img> позволяет лучше контролировать загрузку изображения, подходящего для области просмотра, и при этом дает возможность сканеру предварительной загрузки обнаружить его.
Встраивание слишком большого количества ресурсов
Встраивание – это размещение ресурса внутри HTML-кода. Вы можете встраивать таблицы стилей в элементы <style>, скрипты – в элементы <script>, а практически любые другие ресурсы – с помощью кодировки base64.
Встроенные ресурсы могут загружаться быстрее, чем скачиваемые, поскольку для них не отправляется отдельный запрос. Она находится прямо в документе и загружается мгновенно. Однако у этого подхода есть существенные недостатки:
- Если вы не кешируете HTML-код (а это невозможно, если ответ HTML динамический), встроенные ресурсы никогда не кешируются. Это влияет на производительность, поскольку встроенные ресурсы нельзя использовать повторно.
- Даже если вы можете кешировать HTML, встроенные ресурсы не будут общими для разных документов. Это снижает эффективность кеширования по сравнению с внешними файлами, которые можно кешировать и повторно использовать во всем источнике.
- Если встроить слишком много, сканер предварительной загрузки обнаружит ресурсы в документе позже, потому что скачивание дополнительного встроенного контента займет больше времени.
Рассмотрим эту страницу. В некоторых случаях кандидатом на роль LCP является изображение в верхней части страницы, а CSS находится в отдельном файле, загружаемом элементом <link>. На странице также используются четыре веб-шрифта, которые запрашиваются как отдельные файлы из ресурса CSS.
<img>, но обнаруженное сканером предварительной загрузки, поскольку CSS и шрифты, необходимые для загрузки страницы, загружаются в отдельных ресурсах, что не мешает сканеру предварительной загрузки выполнять свою работу.
Что произойдет, если CSS и все шрифты будут встроены как ресурсы base64?
<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:
Если полезная нагрузка разметки содержится в 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, проверьте в лабораторных инструментах, дает ли он желаемый эффект. Наконец, не стоит предварительно загружать слишком много ресурсов, потому что если вы сделаете приоритетным всё, то приоритетным не будет ничего.