Узнайте, что такое сканер предварительной загрузки браузера, как он повышает производительность и как не мешать ему.
Один упускаемый из виду аспект оптимизации скорости страницы предполагает знание внутреннего устройства браузера. Браузеры выполняют определенные оптимизации для повышения производительности способами, которые мы, разработчики, не можем сделать, но только до тех пор, пока эти оптимизации не будут непреднамеренно сорваны.
Одна из внутренних оптимизаций браузера, которую следует понять, — это сканер предварительной загрузки браузера. В этом посте будет рассказано о том, как работает сканер предварительной загрузки, и, что более важно, о том, как не мешать ему.
Что такое сканер предварительной загрузки?
В каждом браузере есть основной анализатор HTML, который маркирует необработанную разметку и преобразует ее в объектную модель . Все это весело продолжается до тех пор, пока парсер не остановится, когда обнаружит блокирующий ресурс , например, таблицу стилей, загруженную с элементом <link>
, или скрипт, загруженный с элементом <script>
без атрибута async
или defer
.
<link>
для внешнего файла CSS, который блокирует браузер от анализа остальной части документа — или даже отрисовки какой-либо его части — до тех пор, пока CSS не будет загружен и проанализирован.В случае файлов CSS блокируется как синтаксический анализ, так и рендеринг, чтобы предотвратить вспышку нестилизованного контента (FOUC) , когда ненатуральную версию страницы можно ненадолго увидеть перед применением к ней стилей.
![Домашняя страница web.dev в нестилизованном состоянии (слева) и стилизованном состоянии (справа).](https://web.dev/static/articles/preload-scanner/image/the-webdev-home-page-an-d03ddaf5d79f7.png?authuser=1&hl=ru)
Браузер также блокирует синтаксический анализ и рендеринг страницы, когда он встречает элементы <script>
без атрибута defer
или async
.
Причина этого в том, что браузер не может знать наверняка, изменит ли какой-либо конкретный скрипт DOM, пока основной парсер HTML все еще выполняет свою работу. Вот почему общепринятой практикой является загрузка JavaScript в конце документа, чтобы эффекты заблокированного синтаксического анализа и рендеринга стали незначительными.
Это веские причины, по которым браузер должен блокировать как синтаксический анализ, так и рендеринг. Тем не менее, блокирование любого из этих важных шагов нежелательно, поскольку они могут задержать зрелище, задерживая открытие других важных ресурсов. К счастью, браузеры делают все возможное, чтобы смягчить эти проблемы с помощью вторичного анализатора HTML, называемого сканером предварительной загрузки .
<body>
, но сканер предварительной загрузки может просмотреть необработанную разметку, чтобы найти этот ресурс изображения и начать его загрузку до того, как основной парсер HTML разблокирован.Роль сканера предварительной загрузки является спекулятивной , то есть он проверяет необработанную разметку, чтобы найти ресурсы для удобного извлечения до того, как основной анализатор HTML обнаружит их в противном случае.
Как определить, что сканер предварительной загрузки работает
Сканер предварительной загрузки существует из-за заблокированного рендеринга и синтаксического анализа. Если бы этих двух проблем с производительностью никогда не существовало, сканер предварительной загрузки не был бы очень полезен. Ключ к выяснению того, получит ли веб-страница пользу от сканера предварительной загрузки, зависит от этих явлений блокировки. Для этого можно ввести искусственную задержку на запросы, чтобы узнать, где работает сканер предварительной загрузки.
В качестве примера возьмем эту страницу с основным текстом и изображениями с таблицей стилей. Поскольку файлы CSS блокируют как рендеринг, так и синтаксический анализ, вы вводите искусственную задержку в две секунды для таблицы стилей через прокси-сервис. Эта задержка позволяет легче увидеть в сетевом водопаде, где работает сканер предварительной загрузки.
![Сетевая каскадная диаграмма WebPageTest иллюстрирует искусственную задержку в 2 секунды, введенную в таблицу стилей.](https://web.dev/static/articles/preload-scanner/image/the-webpagetest-network-w-888b82cd76bf1.png?authuser=1&hl=ru)
Как вы можете видеть на водопаде, сканер предварительной загрузки обнаруживает элемент <img>
даже тогда, когда рендеринг и синтаксический анализ документа заблокированы . Без этой оптимизации браузер не сможет оперативно извлекать данные в период блокировки, и больше запросов на ресурсы будут последовательными, а не одновременными.
Оставив в стороне этот игрушечный пример, давайте взглянем на некоторые реальные закономерности, в которых можно обойти сканер предварительной загрузки, и что можно сделать, чтобы их исправить.
Внедренные async
скрипты
Допустим, у вас есть HTML в <head>
, который включает в себя встроенный JavaScript, например:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
Внедренные сценарии по умолчанию являются async
, поэтому при внедрении этого сценария он будет вести себя так, как если бы к нему был применен атрибут async
. Это означает, что он запустится как можно скорее и не заблокирует рендеринг. Звучит оптимально, не так ли? Тем не менее, если вы предполагаете, что этот встроенный <script>
идет после элемента <link>
, который загружает внешний файл CSS, вы получите неоптимальный результат:
![На этой диаграмме WebPageTest показано, что сканирование предварительной загрузки завершилось неудачно при внедрении сценария.](https://web.dev/static/articles/preload-scanner/image/this-webpagetest-chart-sh-d83c16fca193.png?authuser=1&hl=ru)
async
скрипт. Сканер предварительной загрузки не может обнаружить сценарий на этапе блокировки рендеринга, поскольку он внедряется в клиент.Давайте разберем, что здесь произошло:
- В 0 секунд запрашивается основной документ.
- Через 1,4 секунды приходит первый байт навигационного запроса.
- Через 2,0 секунды запрашиваются CSS и изображение.
- Поскольку синтаксический анализатор блокирует загрузку таблицы стилей, а встроенный код JavaScript, который внедряет
async
сценарий, появляется после этой таблицы стилей через 2,6 секунды, функциональность, предоставляемая сценарием, недоступна так быстро, как могла бы.
Это неоптимально, поскольку запрос сценария происходит только после завершения загрузки таблицы стилей. Это задерживает выполнение сценария как можно скорее. Напротив, поскольку элемент <img>
доступен для обнаружения в разметке, предоставленной сервером, он обнаруживается сканером предварительной загрузки.
Итак, что произойдет, если вы используете обычный тег <script>
с атрибутом async
вместо внедрения сценария в DOM?
<script src="/yall.min.js" async></script>
Вот результат:
![Сетевой водопад WebPageTest, показывающий, как асинхронный сценарий, загруженный с помощью элемента сценария HTML, по-прежнему доступен для обнаружения сканером предварительной загрузки браузера, даже несмотря на то, что основной анализатор HTML браузера блокируется во время загрузки и обработки таблицы стилей.](https://web.dev/static/articles/preload-scanner/image/a-webpagetest-network-wat-02055245d5ef.png?authuser=1&hl=ru)
async
элемент <script>
. Сканер предварительной загрузки обнаруживает скрипт на этапе блокировки рендеринга и загружает его одновременно с CSS. Может возникнуть соблазн предположить, что эти проблемы можно решить, используя rel=preload
. Это, безусловно, сработает, но может повлечь за собой некоторые побочные эффекты. В конце концов, зачем использовать rel=preload
для решения проблемы, которой можно избежать, не внедряя элемент <script>
в DOM?
![Водопад WebPageTest, показывающий, как подсказка ресурса rel=preload используется для содействия обнаружению внедренного асинхронного сценария, хотя и таким образом, который может иметь непредвиденные побочные эффекты.](https://web.dev/static/articles/preload-scanner/image/a-webpagetest-waterfall-s-2f698a04955f.png?authuser=1&hl=ru)
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, необходимый для работы отложенной загрузки. Изображение обнаружено гораздо позже, чем должно быть.](https://web.dev/static/articles/preload-scanner/image/a-webpagetest-network-wat-6e823c2a1d7a6.png?authuser=1&hl=ru)
В зависимости от размера изображения, который может зависеть от размера области просмотра, он может быть элементом-кандидатом на звание Largest Contentful Paint (LCP) . Когда сканер предварительной загрузки не может спекулятивно получить ресурс изображения заранее (возможно, в тот момент, когда таблица стилей страницы блокирует рендеринг), страдает LCP.
Решение — изменить разметку изображения:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Это оптимальный шаблон для изображений, которые находятся в области просмотра во время запуска, поскольку сканер предварительной загрузки обнаружит и извлечет ресурс изображения быстрее.
![Сетевая каскадная диаграмма WebPageTest, изображающая сценарий загрузки изображения в области просмотра во время запуска. Изображение не загружается лениво, что означает, что оно не зависит от загрузки сценария, а это означает, что сканер предварительной загрузки может обнаружить его раньше.](https://web.dev/static/articles/preload-scanner/image/a-webpagetest-network-wat-5e2602b834b47.png?authuser=1&hl=ru)
Результатом этого упрощенного примера является улучшение LCP на 100 миллисекунд при медленном соединении. Это может показаться не таким уж большим улучшением, но если учесть, что решение заключается в быстром исправлении разметки и что большинство веб-страниц более сложны, чем этот набор примеров. Это означает, что кандидатам LCP, возможно, придется бороться за пропускную способность со многими другими ресурсами, поэтому подобная оптимизация становится все более важной.
Фоновые изображения CSS
Помните, что сканер предварительной загрузки браузера сканирует разметку . Он не сканирует другие типы ресурсов, такие как CSS, которые могут включать выборку изображений, на которые ссылается свойство background-image
.
Как и HTML, браузеры преобразуют CSS в собственную объектную модель, известную как CSSOM . Если внешние ресурсы обнаруживаются при построении CSSOM, эти ресурсы запрашиваются во время обнаружения, а не сканером предварительной загрузки.
Допустим, кандидат LCP вашей страницы — это элемент со свойством CSS background-image
. Вот что происходит при загрузке ресурсов:
![Сетевая каскадная диаграмма WebPageTest, изображающая страницу с кандидатом LCP, загруженным из CSS с использованием свойства фонового изображения. Поскольку изображение-кандидат LCP относится к типу ресурса, который сканер предварительной загрузки браузера не может проверить, загрузка ресурса задерживается до тех пор, пока CSS не будет загружен и обработан, что задерживает время отрисовки кандидата LCP.](https://web.dev/static/articles/preload-scanner/image/a-webpagetest-network-wat-ca60b2c25fa52.png?authuser=1&hl=ru)
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. Время LCP увеличивается примерно на 250 миллисекунд.](https://web.dev/static/articles/preload-scanner/image/a-webpagetest-network-wat-8a47e283c7c66.png?authuser=1&hl=ru)
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 со временем обнаруживается сканером предварительной загрузки.](https://web.dev/static/articles/preload-scanner/image/a-webpagetest-network-wat-ebe04c09d25a6.png?authuser=1&hl=ru)
<img>
, но оно обнаруживается сканером предварительной загрузки, поскольку CSS и шрифты, необходимые для страницы, загружаются в отдельные ресурсы, что не мешает сканеру предварительной загрузки выполнять свою работу.Что же произойдет, если CSS и все шрифты будут встроены в ресурсы base64?
![Сетевая каскадная диаграмма страницы WebPageTest с внешним CSS-файлом, в котором указаны четыре шрифта. Сканер предварительной загрузки значительно задерживается при обнаружении образа LCP.](https://web.dev/static/articles/preload-scanner/image/a-webpagetest-network-wat-2cad0c33c7bdd.png?authuser=1&hl=ru)
<img>
, но встраивание CSS и четырех ресурсов шрифтов в ` ` задерживает обнаружение изображения сканером предварительной загрузки до тех пор, пока эти ресурсы не будут полностью загружены.Влияние встраивания приводит к негативным последствиям для LCP в этом примере — и для производительности в целом. Версия страницы, которая ничего не встраивает, рисует изображение LCP примерно за 3,5 секунды. Страница, на которой все встраивается, не рисует изображение LCP чуть более 7 секунд.
Здесь задействовано нечто большее, чем просто сканер предварительной загрузки. Встраивание шрифтов — не лучшая стратегия, поскольку base64 — неэффективный формат для двоичных ресурсов. Еще одним важным фактором является то, что внешние ресурсы шрифтов не загружаются, если они не определены CSSOM как необходимые. Когда эти шрифты встроены в кодировку Base64, они загружаются независимо от того, нужны они для текущей страницы или нет.
Может ли предварительная загрузка улучшить ситуацию? Конечно. Вы можете предварительно загрузить изображение LCP и сократить время LCP, но раздувание потенциально некэшируемого HTML-кода встроенными ресурсами имеет и другие негативные последствия для производительности. Этот шаблон также влияет на First Contentful Paint (FCP) . В версии страницы, где ничего не встроено, FCP составляет примерно 2,7 секунды. В версии, где все встроено, FCP составляет примерно 5,8 секунды.
Будьте очень осторожны при встраивании данных в HTML, особенно ресурсов в кодировке Base64. В целом не рекомендуется, за исключением очень маленьких ресурсов. Встраивайте как можно меньше, потому что слишком много встраивания — это игра с огнем.
Рендеринг разметки с помощью клиентского JavaScript
В этом нет никаких сомнений: JavaScript определенно влияет на скорость страницы . Разработчики полагаются на него не только для обеспечения интерактивности, но также существует тенденция полагаться на него для доставки самого контента. В некотором смысле это приводит к улучшению опыта разработчиков; но выгоды для разработчиков не всегда перерастают в выгоды для пользователей.
Одним из шаблонов, который может обойти сканер предварительной загрузки, является рендеринг разметки с помощью клиентского JavaScript:
![Сетевой водопад WebPageTest, показывающий базовую страницу с изображениями и текстом, полностью отображаемыми на клиенте с помощью JavaScript. Поскольку разметка содержится в JavaScript, сканер предварительной загрузки не может обнаружить ни один из ресурсов. Все ресурсы дополнительно задерживаются из-за дополнительной сети и времени обработки, которые требуются платформам JavaScript.](https://web.dev/static/articles/preload-scanner/image/a-webpagetest-network-wat-9a4b1c22879f5.png?authuser=1&hl=ru)
Когда полезные данные разметки содержатся и полностью обрабатываются JavaScript в браузере, любые ресурсы в этой разметке фактически невидимы для сканера предварительной загрузки. Это задерживает обнаружение важных ресурсов, что, безусловно, влияет на LCP. В случае этих примеров запрос изображения LCP значительно задерживается по сравнению с эквивалентным интерфейсом, отображаемым на сервере, для которого не требуется JavaScript.
Это немного отклоняется от темы данной статьи, но эффект от рендеринга разметки на клиенте выходит далеко за рамки победы над сканером предварительной загрузки. Во-первых, внедрение JavaScript для обеспечения работы, которая не требует его, приводит к ненужному времени обработки, которое может повлиять на взаимодействие со следующей отрисовкой (INP) . Отображение чрезвычайно больших объемов разметки на клиенте с большей вероятностью приведет к созданию длительных задач по сравнению с тем же объемом разметки, отправляемым сервером. Причина этого (помимо дополнительной обработки, которую включает в себя JavaScript) заключается в том, что браузеры передают разметку с сервера и фрагментируют рендеринг таким образом, что это приводит к ограничению длительных задач. С другой стороны, разметка, отображаемая клиентом, обрабатывается как единая монолитная задача, что может повлиять на INP страницы.
Решение этой ситуации зависит от ответа на следующий вопрос: есть ли причина, по которой разметка вашей страницы не может быть предоставлена сервером, а не отображена на клиенте? Если ответ на этот вопрос «нет», следует рассмотреть возможность рендеринга на стороне сервера (SSR) или статически сгенерированной разметки, поскольку это поможет сканеру предварительной загрузки обнаружить и своевременно получить важные ресурсы заранее.
Если вашей странице требуется JavaScript для придания функциональности некоторым частям разметки страницы, вы все равно можете сделать это с помощью SSR, либо с помощью стандартного JavaScript, либо с помощью гидратации, чтобы получить лучшее от обоих миров.
Помогите сканеру предварительной загрузки помочь вам
Сканер предварительной загрузки — это высокоэффективная оптимизация браузера, которая помогает страницам загружаться быстрее при запуске. Избегая шаблонов, которые лишают его возможности заблаговременно обнаруживать важные ресурсы, вы не только упрощаете разработку для себя, но и создаете лучший пользовательский интерфейс, который обеспечит лучшие результаты по многим показателям, включая некоторые веб-жизненные показатели .
Подводя итог, вот следующие вещи, которые вы захотите вынести из этого поста:
- Сканер предварительной загрузки браузера — это вторичный анализатор HTML, который сканирует перед основным, если он заблокирован, для оперативного обнаружения ресурсов, которые он может получить раньше.
- Ресурсы, которых нет в разметке, предоставленной сервером в первоначальном запросе навигации, не могут быть обнаружены сканером предварительной загрузки. Способы обхода сканера предварительной загрузки могут включать (но не ограничиваться):
- Внедрение ресурсов в DOM с помощью JavaScript, будь то сценарии, изображения, таблицы стилей или что-то еще, что было бы лучше использовать в исходной полезной нагрузке разметки с сервера.
- Ленивая загрузка изображений или iframe над сгибом с помощью решения JavaScript.
- Отрисовка разметки на клиенте, которая может содержать ссылки на подресурсы документа, с помощью JavaScript.
- Сканер предварительной загрузки сканирует только HTML. Он не проверяет содержимое других ресурсов, особенно CSS, которые могут включать ссылки на важные ресурсы, включая кандидатов на LCP.
Если по какой-либо причине вы не можете избежать шаблона, который отрицательно влияет на способность сканера предварительной загрузки ускорять производительность загрузки, рассмотрите подсказку ресурса rel=preload
. Если вы используете rel=preload
, проверьте в лабораторных инструментах, чтобы убедиться, что это дает желаемый эффект. Наконец, не загружайте слишком много ресурсов, потому что, когда вы расставите приоритеты для всего, ничего не будет.
Ресурсы
- «Асинхронные сценарии», внедрённые в скрипты, считаются вредными
- Как предварительный загрузчик браузера ускоряет загрузку страниц
- Предварительная загрузка критически важных ресурсов для повышения скорости загрузки.
- Установите сетевые подключения заранее, чтобы улучшить воспринимаемую скорость страницы.
- Оптимизация самой большой отрисовки контента
Героическое изображение из Unsplash , автор Мохаммад Рахмани .