Оптимизируем выполнение кода

Используйте `defer` чтобы асинхронно загружать блокирующий JavaScript

Когда пользователь открывает страницу, браузер загружает HTML и строит DOM-дерево, затем загружается CSS на основе которого строится модель CSSOM. Из DOM и CSSOM генерируется rendering tree (дерево отрисовки). Построение этого дерева не начнется в случае наличия блокирующего JavaScript. Мы можем явно сказать браузеру не ждать загрузки и выполнения JS, для этого используются атрибуты тега `script` - `defer` и `async`. Лучше использовать `defer` так как `defer` не блокирует парсинг HTML.

Существует несколько заблуждений связанных с `async` и `defer`, также лучше не использовать `async` и `defer` вместе.

Если вы хотите более глубокого понимания механизмов работы `defer` и `async` Виталий Фридман рекомендует обратить внимание на гайд Милики Михайлии (Milica Mihajlija) Building the DOM faster.

Ленивая загрузка тяжелых компонентов с помощью IntersectionObserver и priority Hints

Тяжелые компоненты лучше загружать лениво. Для этого уже есть нативные механизмы. Мы можете установить атрибут `loading` в значение `lazy`. При этом ваши компоненты будут загружаться только после того как будут достаточно близко к текущему вьюпорту браузера.
<!-- Lazy loading for images, iframes, scripts.
Probably for images outside of the viewport. -->
<img loading="lazy" ... />
<iframe loading="lazy" ... />

<!-- Prompt an early download of an asset.
For critical images, e.g. hero images. -->
<img loading="eager" ... />
<iframe loading="eager" ... />
Момент начала ленивой загрузки зависит от типа изображения и типа соединения (3G/4G etc). Мы также можем использовать атрибут importance (high или low) на тегах <script>, <img> или <link> (только для Blink). Это хороший способ управлять приоритетом загрузки изображений. Однако иногда нужен более точный контроль:
<!--
When the browser assigns "High" priority to an image,
but we don’t actually want that.
-->
<img src="less-important-image.svg"
importance="low" ... />

<!--
We want to initiate an early fetch for a resource,
but also deprioritize it.
-->
<link rel="preload" importance="low"
href="/script.js" as="script" />
Самый гибкий способ реализовать ленивую загрузку — использование Intersection Observer API. При помощи intersection observer вы можете следить за пересечением нужного элемента с родительским элементом или с вьюпортом всего документа. Для этого нужно создать новый экземпляр IntersectionObserver и передать ему колбек.

Этот колбек сработает в момент пересечений. Вы можете управлять моментом срабатывания колбека, используя rootMargin. RootMargin определяет что именно считается областью пересечения.

Детали реализации можно посмотреть в руководстве от Алехандро Гарсия Англада (Alejandro Garcia Anglada). Также полезно будет прочитать пост Рахула Нанвани (Rahul Nanwani) о ленивой загрузке изображений и фона. В Google Fundamentals также есть подробное руководство по ленивой загрузке изображений и видео с помощью IntersectionObserver.
Ленивую загрузку можно использовать везде, например, для загрузки переводов и emoji. Именно так Mobile Twitter удалось ускорить выполнение JavaScript на 80%.
Небольшое предостережение: стоит отметить, что ленивая загрузка должна быть исключением, а не правилом. Её неразумно использовать для элементов, которые нужно показать быстро: главных изображений на странице, изображения главных героев или скриптов, необходимых для того, чтобы основная навигация стала интерактивной.

Используете ли вы decoding="async"?

Атрибут decoding="async" позволяет браузеру декодировать изображение вне основного потока. Еще вы можете использовать IntersetionOpberver чтобы показывать заглушку для изображений вне экрана и загружать изображение в момент, когда оно пересекает вьюпорт браузера.

Кроме того, вы можете отложить рендеринг до декодирования с помощью img. decode () или загрузить изображение, если Image Decode API недоступен.

Кэти Хемпениус и Адди Османи в своем докладе Speed at Scale: Web Performance Tips and Tricks from the Trenches рассказывают как использовать анимацию для асинхронной загрузки изображений.

Есть ли у вас критические CSS?

Чтобы ускорить первоначальный рендеринг, чаще всего разработчики собирают все критические стили (стили — необходимые для начального рендеринга) и добавляют их в <head> страницы. Это делается для уменьшения количества обменов пакетами с сервером (RTT). Из-за медленного старта TCP-соединения максимальный размер критического CSS не должен превышать 14КБ. Если вы превысите этот лимит, браузер сделает еще один дополнительный roundtrip. Для генерации критического CSS можно использовать CriticalCSS и Critical. Однако, по опыту Виталия Фридмана, лучше всего собирать критические CSS вручную.

Затем вы можете заинлайнить критические CSS, a все остальные стили загружать лениво при помощи critters Webpack plugin. По возможности используйте условный инлайнинг, применяемый Filament Group, или конвертируйте на лету инлайн-код в статические ассеты.

Если вы загружаете весь CSS асинхронно с помощью библиотек вроде loadCSS, то в этом нет необходимости. Вместо этого вы можете использовать media="print", чтобы обмануть браузер, заставив его загружать CSS асинхронно, но применять загруженные CSS для media="screen".
<!-- Via Scott Jehl.
https://www.filamentgroup.com/lab/load-css-simpler
/ -->

<!-- Load CSS asynchronously, with low priority
-->

<link rel="stylesheet"
  href="full.css"
  media="print"
  onload="this.media='all'" />
При сборе всех критических стилей обычно учитывают только определенную область. Однако для сложных страниц может оказаться полезным добавить к критическим стилям все, что связано с общим layout’ом страницы, чтобы избежать долгих перерасчетов стилей и не ухудшить метрики Core Web Vitals.

Что если пользователь получает URL-адрес, который ведет на страницу, прокрученную до середины, но CSS для этой области еще не загрузились? В этом случае часто скрывают некритичный контент, например, с помощью opacity: 0; в заинлайненом CSS, а потом изменяют это значение на opacity: 1 в догружаемом CSS-файле. У этого способа есть существенный недостаток: пользователи с медленным соединением могут вообще не увидеть содержимое страницы. Поэтому лучше всегда оставлять контент видимым, даже если он неправильно оформлен.

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

Небольшое предостережение: c HTTP/2 вы можете хранить критические стили в отдельном файле и использовать для их доставки server push. К сожалению, у такого подхода есть некоторые проблемы, в том числе и с кешированием (пример есть на 114 слайде презентации Hooman Beheshti’s). В результате, используя такой подход, вы можете ухудшить производительность вашей страницы. Не удивительно, что Chrome планирует отказаться от поддержки server push в будущем.

Поэкспериментируйте с группировкой ваших стилей

Есть ещё несколько способов оптимизировать CSS. Гарри Робертс (Harry Roberts) провел замечательное исследование с весьма удивительными результатами. Например, он выяснил, что хороший эффект даёт разделение основного CSS-файла по каждому медиа-запросу. При этом браузер будет получать критически важный CSS с высоким приоритетом, а все остальное — с низким приоритетом.

Также избегайте размещения <link rel="stylesheet" /> перед сниппетами async. Если скрипты не зависят от CSS, попробуйте разместить блокирующие скрипты перед блокирующими стилями. Если же зависимость есть, выделите эту зависимость в отдельный скрипт и загружайте ее после блокирующего CSS, а остальное — до.

Скотт Джел (Scott Jehl) предлагает кэшировать инлайновый CSS-файл с помощью service worker. Как это делается: к элементу style добавляется ID, при помощи JavaScript вы находите нужный стиль по id и сохраняете его, используя Cache API (с content type — text/css) для использования на последующих страницах.

Динамические стили тоже могут быть дорогими, но только тогда, когда у вас одновременно рендерятся сотни компонентов. Так что, если вы используете CSS-in-JS убедитесь, что ваша CSS-in-JS библиотека оптимизирована для случаев, когда у вас нет зависимостей от темы или свойств компонентов.

В этой статье вы найдёте ещё больше информации о применении подхода CSS-in-JS и его влиянии на производительность.