Узнайте, что такое сканер предварительной загрузки браузера, как он повышает производительность и как можно избежать его использования.
Один из часто упускаемых из виду аспектов оптимизации скорости загрузки страниц — это знание внутренних механизмов браузера. Браузеры выполняют определенные оптимизации для повышения производительности способами, недоступными для разработчиков, — но только до тех пор, пока эти оптимизации не будут непреднамеренно сорваны.
Одна из важных внутренних оптимизаций браузера, которую следует понимать, — это сканер предварительной загрузки. В этой статье мы рассмотрим, как работает сканер предварительной загрузки, и, что более важно, как избежать его помех.
Что такое сканер предварительной загрузки?
В каждом браузере есть основной HTML-парсер, который токенизирует необработанную разметку и преобразует её в объектную модель . Этот процесс продолжается до тех пор, пока парсер не остановится, обнаружив блокирующий ресурс , например, таблицу стилей, загруженную с помощью элемента <link> , или скрипт, загруженный с помощью элемента <script> без атрибутов async или defer .
<link> для внешнего CSS-файла, что блокирует браузеру возможность анализа остальной части документа — или даже его отображения — до тех пор, пока CSS-файл не будет загружен и проанализирован.В случае с CSS-файлами рендеринг блокируется, чтобы предотвратить «мигание» нестилизованного контента (FOUC) , то есть кратковременное отображение нестилизованной версии страницы перед применением к ней стилей.

Браузер также блокирует разбор и отрисовку страницы, когда встречает элементы <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 скрипт. Сканер предварительной загрузки не может обнаружить скрипт на этапе блокировки рендеринга, поскольку он внедряется на стороне клиента.Давайте разберем, что здесь произошло:
- Через 0 секунд запрашивается основной документ.
- Через 1,4 секунды приходит первый байт запроса на навигацию.
- Через 2,0 секунды запрашиваются 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 и сократить время его загрузки, но раздувание потенциально некэшируемого HTML-кода встроенными ресурсами имеет и другие негативные последствия для производительности. На время первой отрисовки контента (FCP) также влияет этот шаблон. В версии страницы, где ничего не встроено, время FCP составляет примерно 2,7 секунды. В версии, где все встроено, время FCP составляет примерно 5,8 секунды.
Будьте очень осторожны со встраиванием кода в HTML, особенно ресурсов, закодированных в base64. В целом это не рекомендуется, за исключением очень маленьких ресурсов. Встраивайте как можно меньше кода, потому что слишком много — это игра с огнём.
Отображение разметки с помощью клиентского JavaScript
Нет никаких сомнений: JavaScript определенно влияет на скорость загрузки страниц . Разработчики полагаются на него не только для обеспечения интерактивности, но и для самой доставки контента. В некотором смысле это улучшает опыт разработчиков, но преимущества для разработчиков не всегда перерастают в преимущества для пользователей.
Один из способов обойти сканер предварительной загрузки — это рендеринг разметки с помощью клиентского JavaScript:

Когда разметка содержится в JavaScript и полностью отображается в браузере, любые ресурсы в этой разметке фактически невидимы для сканера предварительной загрузки. Это задерживает обнаружение важных ресурсов, что, безусловно, влияет на LCP. В случае этих примеров запрос изображения LCP значительно задерживается по сравнению с аналогичным вариантом, отображаемым на сервере, который не требует появления JavaScript.
Это несколько отклоняется от основной темы статьи, но последствия рендеринга разметки на стороне клиента выходят далеко за рамки обхода сканера предварительной загрузки. Во-первых, внедрение JavaScript для обеспечения работы интерфейса, который в нем не нуждается, приводит к ненужному времени обработки, которое может повлиять на время отрисовки до следующей отрисовки (INP) . Рендеринг чрезвычайно больших объемов разметки на стороне клиента с большей вероятностью приведет к длительным задачам по сравнению с тем же объемом разметки, отправляемым сервером. Причина этого — помимо дополнительной обработки, которую включает JavaScript — заключается в том, что браузеры передают разметку с сервера потоком и разбивают рендеринг на части таким образом, что это, как правило, ограничивает длительность задач. Разметка, отрисованная на стороне клиента, с другой стороны, обрабатывается как единая монолитная задача, что может повлиять на время отрисовки до следующей отрисовки страницы.
Решение этой проблемы зависит от ответа на следующий вопрос: есть ли причина, по которой разметка вашей страницы не может быть предоставлена сервером, а не отрисована на стороне клиента? Если ответ на этот вопрос «нет», следует рассмотреть возможность использования серверного рендеринга (SSR) или статически сгенерированной разметки, поскольку это поможет сканеру предварительной загрузки обнаружить и оперативно получить важные ресурсы.
Если для добавления функциональности к некоторым частям разметки вашей страницы требуется JavaScript, вы все равно можете сделать это с помощью SSR, используя либо чистый JavaScript, либо гидратацию , чтобы получить преимущества обоих подходов.
Помогите сканеру предварительной загрузки помочь вам
Предварительная загрузка с помощью сканера — это высокоэффективная оптимизация браузера, которая помогает страницам загружаться быстрее при запуске. Избегая шаблонов, которые мешают ему обнаруживать важные ресурсы заранее, вы не только упрощаете разработку для себя, но и создаете лучший пользовательский опыт, который приведет к лучшим результатам по многим показателям, включая некоторые важные веб-аналитические параметры .
Подводя итог, вот что вам следует усвоить из этой статьи:
- Сканер предварительной загрузки браузера — это вторичный HTML-парсер, который сканирует страницу раньше основного, если тот заблокирован, чтобы, при необходимости, обнаружить ресурсы, которые он может загрузить раньше.
- Ресурсы, отсутствующие в разметке, предоставленной сервером при первоначальном запросе на навигацию, не могут быть обнаружены сканером предварительной загрузки. Способы обхода сканера предварительной загрузки могут включать (но не ограничиваются ими):
- Внедрение ресурсов в DOM с помощью JavaScript, будь то скрипты, изображения, таблицы стилей или что-либо еще, что лучше было бы включить в первоначальную разметку с сервера.
- Ленивая загрузка изображений или iframe в верхней части страницы с помощью JavaScript.
- Отображение на стороне клиента разметки, которая может содержать ссылки на подресурсы документа, с использованием JavaScript.
- Предварительно загруженный сканер сканирует только HTML. Он не проверяет содержимое других ресурсов, в частности CSS, которые могут содержать ссылки на важные элементы, включая LCP-кандидаты.
Если по какой-либо причине вы не можете избежать шаблона, который негативно влияет на способность сканера предварительной загрузки ускорять загрузку, рассмотрите подсказку ресурса rel=preload . Если вы используете rel=preload , протестируйте это в тестовых инструментах, чтобы убедиться, что это дает желаемый эффект. Наконец, не загружайте слишком много ресурсов предварительно, потому что, если вы расставите приоритеты для всего, ничего не останется в приоритете.
Ресурсы
- Внедрение скриптов в асинхронные процессы считается опасным.
- Как загрузчик браузера ускоряет загрузку страниц
- Предварительная загрузка критически важных активов для повышения скорости загрузки.
- Установите сетевые соединения на раннем этапе, чтобы улучшить воспринимаемую скорость загрузки страниц.
- Оптимизация наиболее насыщенной контентом краски
Главное изображение взято с Unsplash , автор — Мохаммад Рахмани .