Настройка окружения

Инструменты для сборки

Не стоит гнаться за модой. Используйте удобное вам окружение, будь то Webpack, Rollup или Gulp.

Среди сборщиков все более популярными становятся Rollup и Snowpack. Webpack продолжает оставаться самым стабильным и популярным инструментом, к нему написаны сотни плагинов для различных оптимизаций. Давайте посмотрим на Roadmap Webpack в 2021. Одна из популярных стратегий — Granular chunking с Webpack в Next. js и Gatsby.
Основная идея этого подхода — каждая страница использует только необходимый ей набор чанков. В Next. js мы можем использовать server-side build manifest для определения того, какие чанки на каких страницах должны загружаться.

Чтобы уменьшить дублирование кода в проектах Webpack, можно использовать granular chunking, включенную по умолчанию в Next. js и Gatsby (Addy Osmani). (Источник фото).
Еще один полезный плагин — SplitChunksPlugin. Он позволяет управлять разбиением вашей сборки на отдельные чанки. Его грамотное использование позволит уменьшить время загрузки страницы и улучшить кэширование. SplitChunksPlugin доступен из коробки в Next. js 9.2 и Gatsby v2.20.7.

Если вы хотите лучше разобраться в Webpack, Виталий Фридман рекомендует несколько отличных ресурсов:
  • Webpack documentation. Очевидно, что начинать стоит с документации. Также можно посмотреть Webpack — The Confusing Bits от Raja Rao и An Annotated Webpack Config от Andrew Welch.
  • Бесплатные курсы Шона Ларкина (Sean Larkin) Webpack: The Core Concepts и Джеффри Вэйя (Jeffrey Way) Webpack for everyone — хорошее начало для глубокого погружения в тему Webpack.
  • Webpack Fundamentals — подробный 4-х часовой курс с Шоном Ларкиным, выпущенный FrontendMasters.
  • Webpack examples. Здесь вы найдёте сотни готовых конфигураций Webpack, сгруппированных по темам и целям. В качестве бонуса конфигуратор, который позволяет вам сгенерировать файл настроек для webpack. В Awesome-webpack множество полезных ресурсов о Webpack: библиотеки, инструменты, статьи, видео, курсы, книги и примеры для Angular, React и framework-agnostic-проектов.
  • The journey to fast production asset builds with Webpack. Кейс от Etsy с рассказом, как команда перешла от использования системы сборки JavaScript на основе RequireJS к Webpack, и как они смогли собирать более 13 200 ассетов в среднем за 4 минуты.
  • Webpack performance tips. Золотая жила знаний от Ивана Акулова. Советы по производительности (раз, два, три, четыре), в том числе о работе с Webpack.
  • Awesome-webpack-perf. Отличный репозиторий с полезными инструментами и плагинами Webpack. Также поддерживается Иваном Акуловым.

Использование native JS modules в продакшен

Помните такую технику cutting-the-mustard technique? Суть в том, чтобы отправлять базовую функциональность в устаревшие браузеры и расширенную — в современные. Обновленный вариант этого метода может использовать ES2017 + <script type = "module">, также известный как module/nomodule pattern (Джереми Вагнер называет этот метод «дифференциальное обслуживание»).

Вам нужно скомпилировать два бандла:
  1. «Обычный» с Babel-плагинами (transform и polifill) для старых браузеров
  2. Бандл с той же функциональностью, но без Babel-плагинов.
Таким образом можно снизить FID. Джереми Вагнер опубликовал большую статью о дифференциальном обслуживании. Он рассказал о том, как встроить его в ваш пайплайн сборки, начиная с настройки Babel, заканчивая тем, какие изменения нужно будет сделать в Webpack и каков профит от всей этой работы.

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

Нативные модули JavaScript. (Увеличить)
Почти всё, что нужно знать о нативных модулях JavaScript, можно прочитать здесь.
Но стоит иметь в виду, что паттерн module/nomodule на некоторых клиентах может дать обратный эффект. Для подобных случаев можно использовать другую версию паттерна дифференциального обслуживания Джереми Вагнера. В этом случае, правда, мы обойдём сканер предварительной загрузки, что может повлиять на производительность самым неожиданным образом.

Кстати, Rollup и Parcel 2 поддерживают нативные модули из коробки. Для Webpack module/nomodule автоматизируетcя с помощью module-nomodule-plugin.

Стоит отметить, что полноценный feature detection нельзя определять, основываясь на версии браузера. Например, дешевые телефоны Android в развивающихся странах в основном работают на Chrome, что позволяет сократить расходы, несмотря на ограниченные возможности процессора и памяти.

Используя Device Memory Client Hints Header, мы сможем точнее определять low-end устройства. На момент написания статьи этот заголовок поддерживается только в Blink (это относится к client hints в целом). Также можно использовать JavaScript API, доступный в Chrome

Tree-shaking, scope hoisting и code-splitting

Tree-shaking — способ убрать из вашей сборки неиспользуемый код. Webpack умеет это делать. Также Webpack и Rollup умеют делать scope hoisting. С Webpack также можно использовать JSON Tree Shaking.

Code splitting — ещё одна функция Webpack, позволяющая разбить код на чанки и загружать их по запросу. Вам же не нужно загружать, парсить и компилировать весь JavaScript сразу. Вам нужно определить точки разбиения, а Webpack позаботится о зависимостях в сборке. Это позволит вам загружать минимум кода в самом начале и догружать нужные части по мере необходимости. У Александра Кондрова есть фантастическое введение в code splitting с помощью Webpack и React.

Посмотрите в сторону preload-webpack-plugin. Он берёт набор эндпоинтов из кода, разделенного на чанки нужным образом, а затем предлагает браузеру предварительно загрузить их с помощью <link rel = "preload"> или <link rel = "prefetch">. Встроенные директивы Webpack также позволяют контролировать элементы preload/prefetch. Но здесь нужно остерегаться ошибок связанных с приоритезацией загрузки.

Как правильно разделить свой код? Отслеживайте, какие фрагменты CSS и JavaScript используются, а какие не используются. Умар Ханса (Umar Hansa) объясняет, как в этом может помочь Code Coverage в Devtools.

Работая с SPA, нужно учитывать — до того, как мы сможем отобразить страницу, потребуется некоторое время для инициализации приложения. В этом случае вам, скорее всего, потребуется кастомное решение, но можно поискать готовые решения. Например, вот статья о том, как отладить производительность в React, а здесь о том, как устранить распространенные проблемы с производительностью React, а вот видео о повышении производительности в Angular. Как правило, большинство проблем с производительностью возникает на этапе начальной загрузки приложения.

Можем ли мы еще улучшить сборку Webpackом?

Рассмотрим несколько плагинов.

Одну из самых интересных ситуаций процесса сборки описал Иван Акулов. Представьте, что у вас есть функция, которую вы вызываете один раз, сохраняете ее результат в переменной, а затем не используете эту переменную. Tree shaking удалит переменную, но не функцию. Однако, если функция нигде не используется, её лучше удалить. Для этого добавьте перед вызовом функции / * # __ PURE __ * /, который поддерживается Uglify и Terser. Готово!
А вот ещё несколько инструментов, которые рекомендует Иван:
  • Purgecss-webpack-plugin. Удаляет неиспользуемые классы, особенно полезен при работе с Bootstrap или Tailwind.
  • Используйте optimization.splitChunks: 'all' вместе с плагином split-chunks. Включите optimization.runtimeChunk: true. Это выделит webpack runtime в отдельный блок, а также улучшит кеширование.
  • Плагин google-fonts-webpack загружает файлы шрифтов, чтобы вы могли раздавать их со своего сервера.
  • Плагин workbox-webpack позволяет создать сервис-воркер с настройкой предварительного кэширования для всех ваших ресурсов webpack. Здесь вам поможет исчерпывающее руководство Service Worker Packages. Или используйте preload-webpack-plugin, чтобы сгенерировать preload/prefetch для всех чанков. 
  • Плагин speed-measure-webpack измеряет скорость и даёт представление о том, какие этапы процесса сборки являются наиболее медленными.
  • Плагин duplicate-package-checker-webpack предупреждает о дублированиях в сборке.
  • Используйте изоляцию области видимости и динамически сокращайте имена классов CSS во время компиляции.

Можно ли перенести JavaScript в Web Worker?

Чтобы уменьшить TTI, можно перенести тяжелый JavaScript в Web Worker.

Все операции DOM выполняются в основном потоке. С помощью веб-воркеров мы можем перенести некторые тяжёлые операции в фоновый процесс, выполняющийся в другом потоке.

Есть несколько интересныx материалов на эту тему: prefetching data and Progressive Web Apps, Comlink. А здесь — интересные примеры использования веб-воркеров.

В Chrome 80 и выше появился новый режим для веб-воркеров, обеспечивающий лучшую производительность модулей JavaScript — module workers. Теперь можно изменить загрузку и выполнение скриптов с script type="module". Плюс можно использовать динамический импорт для ленивой загрузки кода, не блокируя работу воркера.

Прежде чем начать работу с Web Worker, Виталий Фридман рекомендует обратиться к следующим источникам:
Используйте веб-воркеры, чтобы надолго не блокировать основной поток. (Источник изображения) (Увеличить).
Обратите внимание, что Web Workers не имеют доступа к DOM.

Можно ли перенести JavaScript в Web Worker?

Мы можем переложить тяжелые вычислительные задачи на WebAssembly (Wasm). Он отлично поддерживается браузерами и в последнее время стал весьма полезным, поскольку взаимодействие JavaScript и Wasm становится все быстрее. Поддерживается даже в облаке Fastly.

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

Вот что можно почитать и посмотреть, если вы хотите узнать больше о WebAssembly:
  • Лин Кларк (Lin Clark) написала подробную серию статей о WebAssembly, а Милица Михайлия (Milica Mihajlija) дала общий обзор того, как и почему нужно запускать нативный код в браузере
  • How We Used WebAssembly To Speed Up Our Web App By 20X (Case Study). В статье рассказывается о том, как WebAssembly позволило значительно ускорить приложение.
  • Патрик Хаманн (Patrick Hamann), рассказывает о роли WebAssembly и о потенциальных проблемах использования Wasm, развенчивая некоторые мифы.
  • Введение в WebAssembly от Google Codelabs. 60-минутный курс, в котором вы узнаете, как скомпилировать код на Cв WebAssembly, а затем вызывать Wasm прямо из JavaScript.
  • В своём выступлении на Google I/O talk Алекс Данило (Alex Danilo) объяснил WebAssembly и то, как всё это работает.
  • Бенедек Гаджи (Benedek Gagyi) поделился практическим примером использования WebAssembly.

Раздаем legacy-код только старым браузерам?

ES2017 отлично поддерживается всеми современными браузерами, поэтому можно использовать babelEsmPlugin чтобы собирать только тот код, который не поддерживается нужными вам браузерами. Хуссейн Джирде (Houssein Djirdeh) и Джейсон Миллер (Jason Miller) недавно опубликовали исчерпывающее руководство о том, как собирать и раздавать как legacy так и современный JavaScript при помощи Webpack и Rollup. Вот этот инструмент позволяет оценить, сколько байт JavaScript можно «сэкономить».

Нативные модули JavaScript поддерживаются во всех основных браузерах, просто используйте script type = "module", а старые пусть загружают легаси-сборки с помощью script nomodule.
Сейчас можно использовать нативные JavaScript модули вообще без дополнительных сборщиков. Заголовок <link rel = "modulepreload"> позволяет начать раннюю (и высокоприоритетную) загрузку модуля. При этом мы сообщаем браузеру, что именно ему нужно получить, чтобы он ни на чём не зависал. Джейк Арчибальд (Jake Archibald) опубликовал подробную статью с разбором ошибок и разных нюансов, которые следует учитывать при работе с нативными JavaScript модулями.

Скриншот из статьи Джейка Арчибальда (Увеличить)

Найдите и постепенно перепишите устаревший код

В проектах-долгожителях копится legacy-код. Пересмотрите свои зависимости и оцените, сколько времени потребуется на рефакторинг или переписывание проблемных участков. После всех исследований можно постепенно, начать переписывать старый код.

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

Найдите и постепенно перепишите устаревший код

Инструмент CSS and JavaScript code coverage в Chome позволяет понять, какая часть вашего кода реально используется. Обнаружив неиспользуемый код, начните загружать его лениво с помощью import (). Затем еще раз запустите code coverage и убедитесь что загружается меньше кода чем раньше.

Чтобы собрать code coverage репорт можно использовать Puppeteer. Хром позволяет также экспортировать code coverage report. Как отмечает Andy Davies (Энди Дэвис) вы можете собирать такой репорт как для старых, так и для современных браузеров

Можно использовать Puppeteer Recorder и Puppeteer Sandbox сценариев Puppeteer и Playwright. (Увеличить)
Кроме того, purgecss, UnCSS и Helium позволяют удалить неиспользуемые стили из CSS. А если вам кажется, что где-то используется подозрительный фрагмент кода, можно последовать совету Гарри Робертса: создайте прозрачный GIF размером 1x1px для определенного класса и поместите его в каталог dead/, например /assets/img/dead/comments.gif. Затем установите это изображение в качестве фона для соответствующего селектора в вашем CSS и подождите несколько месяцев, появится ли файл в ваших логах. Если не появится, значит, это устаревший компонент и он не отображался на экране. Теперь его можно удалить навсегда.

Для тех, кто ищет приключений, можно даже автоматизировать сбор неиспользуемого CSS с помощью DevTools.
В своей статье Бенедикт Рётч (Benedikt Rötsch) показал, что переход с Moment. js на date-fns может сэкономить около 300 мс для первоначальной отрисовки при работе в сетях 3G и с дешёвыми мобильными устройствами. (Увеличить)

Уменьшайте размеры JavaScript-бандлов

Скорее всего, вы используете целые библиотеки JavaScript (хотя вам нужна лишь небольшая часть), а также устаревшие полифиллы для браузеров или просто дублирующийся код. Чтобы убрать лишнее, используйте webpack-libs-optimizations, которые удаляют неиспользуемые методы и полифиллы в процессе сборки.

Мыслите стратегически, проверяя полифиллы, которые вы отправляете в браузеры. Здесь вам поможет polyfill.io.

Проверка вашего бандла должна стать частью вашего рабочего процесса! Скорее всего, уже есть более легкие альтернативы тяжелым библиотекам, которые вы добавили много лет назад. Есть множество инструментов, которые помогут вам принять взвешенное решение о зависимостях и, при необходимости, подобрать альтернативу:
Вместо того чтобы использовать развесистую библиотеку, вы можете урезать ваш фреймворк и скомпилировать его в сырой JS-bundle, который включает в себя только нужный код. Это умеют делать Svelte и Rawact Babe Pluginl, который во время сборки превращает React компоненты в нативные операции с DOM. Зачем? Как объясняют мэйнтейнеры, «react-dom включает код для всех возможных компонетов/HTML элементов, код для инкрементного рендеринга, свой планировщик, и много кода для правильной работы с DOM events. Есть приложения, которым при начальной загрузке это все не нужно.

size-limit проверяет размер бандла, а также выдаёт подробную информацию о времени выполнения JavaScript. (Увеличить)

Используйте частичную гидрацию (partial hydration)

Нам нужен способ грузить как можно меньше JS на клиенте. Здесь может помочь частичная гидрация. Идея довольно проста: вместо того, чтобы рендерить все на сервере (SSR) и отправлять сразу все на клиент, мы отправляем, а затем гидрируем (hydrate), небольшие фрагменты-приложения.

В статье Лукаса Бомбаха (Lukas Bombach) описан пример частичной гидрации для новостного сайта Welt.de. В репозитории Лукаса вы найдёте все пояснения и код.

А вот ещё несколько вариантов:
Джейсон Миллер опубликовал демо прогрессивной гидрации в React: демо 1, демо 2, демо 3 (также доступны на GitHub). А ещё можно посмотреть в сторону react-prerendered-component.

Стратегии для React/SPA

Ваше SPA тормозит? Джереми Вагнер исследовал влияние производительности client-side фреймворка на различные устройства. В результате, Джереми предлагает вот такую стратегию для SPA на React (с небольшими изменениями она подойдёт и для других фреймворков):
  • Превращать stateful-компоненты в stateless-компоненты всегда, когда это возможно.
  • По возможности пререндерить stateless-компоненты, чтобы минимизировать время отклика сервера. Рендеринг только на сервере.
  • Пререндерите простые stateful-компоненты на сервере, а их интерактивную часть реализуйте с помощью независмых от фреймоворка event-lintenerов.
  • Если нужно гидрировать stateful-компоненты на клиенте, используйте ленивую гидрацию и запускайте ее только после взаимодействия с компонентом.
  • Начинайте ленивую гидрацию компонентов когда основной поток не занят. Для этого используйте функцию requestIdleCallback.
Ещё больше интересных стратегий вы найдёте в этих источниках:

Используйте predictive prefetching

Вы можете использовать специальные эвристики для определения нужного момента для загрузки ваших JS-чанков Guess.js — использует Google Analytics чтобы предугадать на какую следующую страницу перейдет пользователь Guess. js использует машинное обучение для создания подобного прогноза и предварительной загрузки JavaScript, который потребуется на каждой последующей странице.

Для каждого интерактивного элемента рассчитывается вероятность того что пользователь с ним провзаимодействует. Нужные скрипты префетчатся на основе этой вероятности. Эту технологию можно интегрировать в приложение Next.js, Angular и React. Существует плагин Webpack, автоматизирующий процесс настройки.

Нужно что-то менее сложное? DNStradamus выполняет dns-prefetching для внешних ссылок по мере их появления во вьюпорте. Quicklink, InstantClick и Instant.page — небольшие библиотеки позволяющие префетчить содержимое ссылок, находящихся во вьюпорте. Это позволяет ускорять переходы между страницами.

Если вы хотите лучше вникнуть во все это, Виталий Фридман советует посмотреть доклад The Art of Predictive Prefetch.

Используйте максимум возможностей вашего JS-движка

Посмотрите, какие движки JavaScript преобладают в вашей пользовательской базе, и выберите способ их оптимизации. Например, при оптимизации для V8 (используется в Blink-браузерах, среде выполнения Node. js и Electron) используйте script streaming.

Script streaming позволяет парсить async- или defer-скрипты в отдельном фоновом потоке, сразу после начала загрузки. В некоторых случаях это улучшает время загрузки страницы до 10%. Чтобы браузеры смогли обнаружить скрипт как можно раньше и начали парсить его в фоновом потоке, используйте <script defer> в <head>.

Вы также можете использовать V8 code caching, разделяя библиотеки и код, использующий их. Можно наоборот, объединить библиотеки и код в одном скрипте, сгруппировать маленькие файлы, избегая инлайн-скриптов. А можно даже использовать v8-compile-cache.

Есть еще несколько хороших практик:
  • Clean Code Concepts for JavaScript, большая коллекция паттернов для написания читабельного, переиспользуемого и удобного для рефакторинга кода.
  • Сжимать данные Compression Stream API, например, в gzip перед загрузкой данных (Chrome 80+).
  • Используйте эти руководства, чтобы найти и устранить утечки памяти в вашем приложении.
  • Можно значительно уменьшить размер бандла, если избегать реэкспорта.
  • Улучшить производительность скроллинга можно с помощью пассивных EventListeners, установив флаг в параметре options. Так браузеры смогут прокручивать страницу сразу, а не после завершения работы EventListener.
  • Улучшить планирование JavaScript поможет isInputPending () — новый API, который пытается уменьшить разрыв между загрузкой и отзывчивостью с помощью прерываний для ввода.
  • EventListener можно автоматически удалить после его выполнения.
  • Недавно Firefox выпустил Warp, значительное обновление SpiderMonkey (поставляется в Firefox 83), Baseline Interpreter, а также несколько стратегий JIT-оптимизации.