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

Статьи по этой теме на SmashingMagazine:

Несколько недель назад я добавил поддержку предзагрузки в Chrome Canary и если не возникнет каких-либо непредвиденных багов, она будет в стабильной версии Chrome в середине апреля. Но что такое предзагрузка? Что она делает и чем она может быть полезна?

Предварительная загрузка (<link rel="preload">) это декларативная директива выборки.

Если объяснять по-человечески, то это способ сообщить браузеру, чтобы он начал скачивать определенный ресурс, так как мы, авторы страницы (администраторы сервера или разработчики) знаем, что очень скоро этот ресурс потребуется браузеру.

Разве у нас нет такой возможности?

Что-то похожее есть. <link rel="prefetch"> появился в вебе давно и поддерживается в большинстве браузеров. Некоторое время назад в Chrome появился и <link rel="subresource">. Так что нового в предзагрузке? Чем она отличается от остальных директив? Они же все говорят браузеру о выборке ресурсов, не так ли?

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

Префетчинг (<link rel="prefetch">) это директива, сообщающая браузеру о необходимости выборки ресурса, который понадобится при переходе на следующую страницу. Это означает, что выборка ресурса будет происходить с крайне низким приоритетом (ведь браузер знает, что ресурсы для текущей страницы более важны, чем ресурсы, которые понадобятся на следующей). Это значит, что основное использование префетчинга это ускорение загрузки следующей страницы, а не текущей.

Субресурс (<link rel="subresource">) был запланирован для решения задач на текущей странице, но в большинстве вопросов он оказывается неэффективен. Так как разработчик не можем задать приоритет загрузки ресурсов, браузер (точнее Chrome и браузеры на основе Chromium) скачивает их с довольно низким приоритетом, в большинстве случаев это означает, что запрос к ресурсу занимает примерно такое же количество времени, как и без использования субресурса.

Как можно добиться лучшей предзагрузки?

Предзагрузка создана для работы на текущей странице, также как субресурс, но с одним небольшим, но важным отличием. У предзагрузки есть атрибут as, позволяющий браузеру делать вещи, которые невозможны с префетчингом и субресурсом:

  • Браузер может задать правильный приоритет ресурсов, в соответствии с которым они будут загружаться — не будет ни задержки важных ресурсов, ни упорного выкачивания второстепенных.
  • Браузер может обеспечить соответствие ресурса директивам политики безопасности контента и не выходить на ненадежный сервер.
  • Браузер может отправить соответствующие заголовки Accept на основе типа ресурса (например, image/webp при выборке изображений).
  • Браузер знает тип ресурса и поэтому может определить, может ли он использоваться при последующих запросах к тому же ресурсу.

Предзагрузка также отличается тем, что в ней есть функциональное событие onload (как минимум в Chrome это событие не работает для двух остальных значений rel).

Также предзагрузка не блокирует событие окна onload, разумеется, за исключением случаев, когда предзагружаемый ресурс запрашивает другой ресурс, блокирующий это событие.

Сочетание всех этих характеристик реализует целый спектр возможностей, ранее нам недоступных.

Рассмотрим их по отдельности.

Загрузка “поздно-обнаруживаемых” ресурсов

Основным способом использования предзагрузки будет ранняя загрузка “поздно-обнаруживаемых” ресурсов. Хотя большинство ресурсов в разметке предзагрузчик браузера обнаруживает достаточно быстро, не все ресурсы указаны в разметке страницы. Некоторые ресурсы спрятаны в CSS и JavaScript и браузер не может узнать о них до того, как они понадобятся. Поэтому во многих случаях эти ресурсы приводят к задержке рендеринга, вывода текста или загрузки критических частей страницы.

Теперь у вас есть средства сказать браузеру: “Эй, браузер! Вот этот ресурс тебе обязательно понадобится, поэтому загружай его сейчас”.

В коде эта фраза выглядит следующим образом:

<link rel="preload" href="late_discovered_thing.js" as="script">

Атрибут as указывает тип скачиваемого ресурса, у него могут быть следующие значения:

  • "script"
  • "style"
  • "image"
  • "media"
  • "document"

Полный список значений есть в спецификации.

Пропуск атрибута as или задание некорректного значения эквивалентно XHR-запросу, в котором браузер не знает, что он выбирает, вследствие чего выборка происходит с низким приоритетом.

Ранняя загрузка шрифтов

Одной из популярных разновидностей “поздно обнаруживаемых критических ресурсов” являются веб-шрифты. С одной стороны, в большинстве случаев они критичны для рендеринга текста на странице (пока у нас не будут реализованы свойства font-display). С другой, шрифты находятся глубоко в недрах CSS и даже если браузерный предзагрузчик распарсил CSS, он не может быть уверен в том, что они понадобятся, пока не разберется с привязкой вызывающих их селекторов к конкретным узлам DOM. Хотя в теории браузеры должны с этим разбираться, ни один из них не делает этого, потому как это приводило бы к ложным загрузкам в случаях, когда стилевые декларации переписываются в последующих строках CSS.

В общем, все сложно.

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

<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

Один момент следует уточнить: вы должны добавить атрибут crossorigin при выборке шрифтов, так как она производится в анонимном режиме CORS. Да, даже в том случае, если шрифты находятся на том же хосте, что и страница. Увы.

Атрибут type обеспечивает предзагрузку этого ресурса только в тех браузерах, которые поддерживают этот тип шрифта. На данный момент предварительная загрузка поддерживается только в Chrome и он прекрасно поддерживает WOFF2, но в будущем предзагрузка может появиться и в других браузерах и мы не можем гарантировать, что в них будет поддержка WOFF2. То же самое относится и к остальным типам ресурсов, которые вы предварительно загружаете и у которых нет стопроцентной поддержки в браузерах.

Динамическая загрузка без запуска

Следующий интересный сценарий, который становится доступным, это скачивание нужного вам ресурса, который вы не собираетесь сразу запускать. Например, в случае, если вы хотите выполнить скрипт в определенный период существования страницы, не имея контроля над скриптом (то есть без возможности добавить функцию runNow()).

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

Так что вы можете сделать?

Без предварительной загрузки немногое. В отдельных случаях вы могли выполнить содержимое скрипта с помощью eval(), но это не всегда осуществимо и влечет побочные эффекты. Но с предварительной загрузкой все возможно!

var preload = document.createElement("link");
link.href = "myscript.js";
link.rel = "preload";
link.as = "script";
document.head.appendChild(link);

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

var script = document.createElement("script");
script.src = "myscript.js";
document.body.appendChild(script);

Асинхронный загрузчик в разметке

Другой крутой хак это использование обработчика onload для создания некоего подобия асинхронного загрузчика прямо в разметке. Скотт Джел первым экспериментировал с этим, как с частью своей библиотеки loadCSS. Мы можем использовать следующий краткий код:

<link rel="preload" as="style" href="async_style.css" onload="this.rel='stylesheet'">

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

Это также работает и с асинхронными скриптами.

Но вы скажете, что у нас уже есть <script async>? Да, <script async> это замечательно, но эта методика блокирует событие window.onload. В некоторых случаях, это именно то, что вам нужно, но в других случаях это, наоборот, мешает.

Предположим, вы хотите загрузить скрипт для веб-аналитики. Вы хотите загрузить его быстро (чтобы не потерять пользователей, ради изучения которых вы его добавили) и вы не хотите, чтобы его загрузка вызывала задержку для метрик, влияющих на ощущения пользователей, в первую очередь window.onload. Вы можете утверждать, что событие onload не является единственной значимой метрикой и вы будете правы, но все равно приятно остановить вращение спиннера загрузки немного раньше.

С предварительной загрузкой это делается с легкостью:

<link rel="preload" as="script" href="async_script.js"
onload="var script = document.createElement('script');
        script.src = this.href;
        document.body.appendChild(script);">

Возможно, это не лучшая идея — включать длинные функции JavaScript в качестве атрибутов onload, вы можете вынести этот фрагмент в отдельную функцию.

Загрузка с медиа-запросом

Так как предзагрузка реализована с помощью тега link, у нее есть атрибут media (пока это не поддерживается в Chrome, но скоро будет). Этот атрибут позволяет активировать условную загрузку ресурса.

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

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

Что мы можем сделать, чтобы браузер узнал об этих ресурсах как можно раньше?

Вы угадали! Использовать предварительную загрузку.

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

<link rel="preload" as="image" href="map.png" media="(max-width: 600px)">

<link rel="preload" as="script" href="map.js" media="(min-width: 601px)">

Заголовки HTTP

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

Примеры с такими заголовками ответов HTTP могут выглядеть так:

Link: <thing_to_load.js>;rel="preload";as="script"

Link: <thing_to_load.woff2>;rel="preload";as="font";crossorigin

HTTP-заголовки могут быть удобны, когда за разметку сайта и его оптимизацию отвечают разные люди. Например, это может быть внешняя организация, занимающаяся сканированием и оптимизацией сайта (автор оригинальной статьи работает в одной из них).

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

Детектирование поддержки предварительной загрузки

Последнее замечание: в некоторых из примеров мы полагаемся на тот факт, что предзагрузка поддерживает базовый функционал типа загрузки скриптов или стилей. Что произойдет в браузерах, не поддерживающих это?

Все поломается!

Нам это не нужно. Поэтому при разработке предварительной загрузки мы поменяли спецификацию DOM, реализовав детектирование функционала с помощью ключевых слов rel.

Вот пример кода для детектирования поддержки функционала:

var DOMTokenListSupports = function(tokenList, token) {
  if (!tokenList || !tokenList.supports) {
    return;
  }
  try {
    return tokenList.supports(token);
  } catch (e) {
    if (e instanceof TypeError) {
      console.log("The DOMTokenList doesn't have a supported tokens list");
    } else {
      console.error("That shouldn't have happened");
    }
  }
};

var linkSupportsPreload = DOMTokenListSupports(document.createElement("link").relList, "preload");
if (!linkSupportsPreload) {
  // Dynamically load the things that relied on preload.
}

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

Разве HTTP/2 Push не решает те же проблемы?

Не совсем. Хотя эти технологии частично перекрываются, в большинстве случаев они дополняют друг друга.

У HTTP/2 Push есть преимущество в виде возможности отправлять ресурсы, которые браузер еще не запросил. Это значит, что с помощью Push можно отправлять ресурсы даже до отправки HTML. Это также можно использовать для отправки ресурсов по открытому соединению HTTP/2, не требуя ответа, к которому могут присоединяться заголовки.

С другой стороны, предварительная загрузка может помочь в ряде случаев, которые HTTP2 не решит. Как мы видели, с предзагрузкой приложение знает о том, что идет загрузка ресурса и может быть уведомлено о том, что ресурс полностью загружен. Это выходит за пределы возможностей HTTP/2 Push. Также HTTP/2 Push не может использоваться для сторонних ресурсов, у предварительной загрузки такого ограничения нет.

Также, HTTP/2 Push не может использовать кэш браузера и неглобальные куки. Хотя с кэшем можно разобраться с помощью новой спецификации cache digest, с неглобальными куки сделать ничего нельзя, поэтому Push нельзя использовать для ресурсов, зависящих от таких куки. В этом случае, вам поможет предварительная загрузка.

Еще одно преимущество предварительной загрузки это возможность согласования контента, в отличие от HTTP/2 Push. Это значит, что если вы хотите использовать клиентские хинты для выбора нужного изображения или заголовки Accept: для выбора лучшего формата, HTTP/2 Push не сможет вам помочь.

Заключение

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

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