Уменьшение размеров бандлов с помощью Webpack Analyzer и React Lazy/Suspense

Автор оригинала: Eyal Eizenberg
  • Перевод
По мере того, как растёт сложность клиентских приложений, размеры их бандлов становятся всё больше и больше. В этой ситуации сильнее всего страдают люди, вынужденные, по разным причинам, пользоваться медленными интернет-соединениями. При этом с каждым днём всё становится только хуже.



Автор статьи, перевод которой мы сегодня публикуем, работает в Wix. Он хочет рассказать о том, как смог уменьшить размер одного бандла примерно на 80%, используя Webpack Analyzer и React Lazy/Suspense.

Насколько рано стоит приступать к оптимизации?


Если вы только начали работу над своим новым веб-приложением, то вы, возможно, пытаетесь сосредоточить усилия на том, чтобы оно, так сказать, «оторвалось от земли», стараетесь сделать так, чтобы оно заработало. Вы, вероятно, не обращаете особого внимания на производительность или на размер бандла. Я могу это понять. Однако мой опыт подсказывает, что о производительности и о размерах бандлов стоит заботиться с самого начала. Хорошая архитектура приложения и своевременные «размышления о будущем проекта» сэкономят вам, в долгосрочной перспективе, немало времени и помогут не накопить серьёзного технического долга. Очевидно то, что сложно заранее всё «предвидеть», но вам стоит очень постараться сделать всё правильно с первого дня работы над проектом.

Вот пара отличных инструментов, которые, как мне кажется, нужно использовать с самого начала. Эти инструменты помогут распознать «проблематичные» NPM-пакеты даже ещё до того, как они займут сколько-нибудь важное место в приложении.

▍Bundlephobia


Bundlephobia — это сайт, который позволяет узнавать о том, насколько некий NPM-пакет увеличит размер бандла. Это — отличный инструмент, помогающий программисту принимать правильные решения, касающиеся выбора пакетов сторонних разработчиков, которые могут ему понадобиться. Bundlephobia помогает проектировать архитектуру приложения так, чтобы его размер не оказался бы слишком большим. На следующем рисунке показаны результаты проверки популярной библиотеки для работы со временем, которая называется moment. Можно видеть, что эта библиотека довольно велика — почти 66 Кб в сжатом виде. Для многих пользователей, работающих на скоростном интернете, это — ничто. Однако стоит обратить внимание на то, каким становится время загрузки этого пакета в 2G/3G сетях. Оно, соответственно, составляет 2.2 и 1.32 секунды. И, обратите внимание, речь идёт только об одном этом пакете.


Результаты анализа пакета moment средствами Bundlephobia

▍Import Cost


Import Cost — это весьма интересное расширение для множества популярных редакторов кода (у него более миллиона загрузок для VS Code). Оно умеет показывать «стоимость» импортированных пакетов. Особенно мне в нём нравится то, что оно помогает выявлять проблемные области приложения прямо во время работы над ним. На следующем рисунке (он взят с GitHub-страницы Import Cost) показан отличный пример воздействия на размеры проекта разного подхода к импорту сущностей. Так, импорт единственного свойства uniqueId из Lodash приводит к импорту в проект всей этой библиотеки (70 Кб). А если напрямую импортировать функцию uniqueId, то к размеру проекта будет добавлено всего 2 Кб. Подробнее об Import Cost можно почитать здесь.


«Стоимость» импорта в проект всей библиотеки Lodash и только одной конкретной функции из этой библиотеки

Дело о неоправданно больших бандлах


Итак, вы создали своё замечательное приложение. Оно отлично работает на вашем скоростном интернете и на вашем мощнейшем компьютере, набитом оперативной памятью. Вы выпустили его в реальный мир. Через некоторое время к вам начали поступать жалобы от пользователей или от ваших же аналитиков. Эти жалобы касались времени загрузки приложения. Нечто подобное недавно случилось со мной, когда мы, в Wix, зарелизили новую возможность, над которой я работал.

Для того чтобы немного ввести вас в курс дела — давайте поговорим об этой новой возможности. Это — новый прогресс-бар, расположенный в верхней части боковой панели интерфейса настройки сайтов пользователей. Цель этого механизма заключается в том, чтобы привлечь внимание пользователя к различным шагам, которые ему нужно выполнить для того, чтобы у его бизнес-проекта было бы больше шансов на успех (подключение SEO, добавка регионов, в которые осуществляется доставка товаров, добавление первого продукта, и так далее).

Прогресс-бар обновляется автоматически, подключаясь к серверу с использованием веб-сокетов. Когда пользователь завершает выполнение всех рекомендованных шагов — выводится всплывающее окно с поздравлениями. После того, как это окно закрывают, прогресс-бар скрывается и больше никогда не выводится при работе с тем сайтом, в настройке которого он использовался. Вот как выглядит то, о чём мы только что говорили.


Прогресс-бар и окно с поздравлениями

Что же случилось? Почему наши аналитики жаловались мне на то, что время загрузки страницы выросло? Когда я изучил состояние дел, воспользовавшись вкладкой Network инструментов разработчика Chrome, мне сразу же стало ясно то, что мой бандл оказался довольно большим. А именно — его размер составлял 190 Кб.


Размер бандла, выясненный с помощью инструментов разработчика Chrome

«Почему для этой мелочи нужен сравнительно большой бандл?», — подумал я тогда. А правда — почему?

▍Поиск проблемных мест в бандле


После того, как я понял, что размер бандла слишком велик, пришло время выяснить причину этого. Здесь мне пригодился Webpack Bundle Analyzer — отличный инструмент для выявления проблемных мест бандлов. Он открывает новую вкладку браузера и показывает сведения о зависимостях.

Вот что получилось после того, как я проанализировал бандл с помощью этого инструмента.


Результаты работы Webpack Bundle Analyzer

С помощью анализатора я смог обнаружить «преступника». Здесь использовался пакет lottie-web, что добавляло к размеру бандла 61.45 Кб. Lottie — это весьма приятная JavaScript-библиотека, которая позволяет, применяя стандартные средства браузера, выводить анимации, созданные в Adobe After Effect. В моём случае было так, что нашему дизайнеру нужна была приятная анимация, выполнявшаяся при появлении окна с поздравлениями. Он эту анимацию создал и дал её мне в виде JSON-файла, который я передал пакету Lottie и получил красивую анимацию. В дополнение к пакету lottie-web у меня был ещё и JSON-файл с описанием анимации. Размер этого файла составлял 26 Кб. В результате библиотека Lottie, JSON-файл, а также некоторые вспомогательные маленькие зависимости «стоили» мне примерно 94 Кб. И это — всего лишь анимация окна с поздравлениями пользователю. Пользователь, когда эти поздравления видел, должен был радоваться. Мне же от всего этого было грустно.

Технологии React Lazy/Suspense приходят на помощь


После того, как я обнаружил причину проблемы, пришло время эту проблему решать. Ясно было то, что не нужно было загружать в самом начале работы всё, что требовалось для анимации. На самом деле, существовала немалая вероятность того, что в ходе текущей сессии пользователя ему не придётся показывать окно с поздравлениями. Тогда я ознакомился с недавно появившимися технологиями React Lazy/Suspense и подумал, что сейчас у меня, вероятно, появилась хорошая возможность их испытать.

Если вы не знакомы с концепцией «ленивых» (lazy) компонентов, то знайте, что их смысл заключается в разделении приложения на небольшие фрагменты кода. Загрузка этих фрагментов выполняется только тогда, когда они нужны. В моём случае это выражалось в том, что мне нужно было выделить из основного функционала прогресс-бара компонент, который был ответственен за показ поздравления. Загружать этот компонент надо было только тогда, когда пользователь завершал выполнение рекомендованной последовательности шагов.

В React 16.6.0 (и в более новых версиях) есть простой API, который предназначен для рендеринга ленивых компонентов. Речь идёт о React.lazy и React.Suspense. Рассмотрим пример:

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <React.Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </React.Suspense>
    </div>
  );
}

У нас имеется компонент, который выводит элемент <div>, а в нём — компонент Suspense, который оборачивает компонент OtherComponent. Если посмотреть на первую строчку этого примера — можно увидеть, что OtherComponent не импортируется в код напрямую. Обычно подобный импорт выглядит как import OtherComponent from './OtherComponent';.

Вместо этого команда импорта оформлена в виде функции, которая принимает путь к файлу. Этот механизм работает благодаря тому, что в Webpack есть встроенные средства разделения кода. Когда в коде присутствует подобная конструкция — возвращается промис, который, после загрузки файла, разрешается с содержимым этого файла. Наша команда импорта обёрнута в функцию React.lazy.

В материалах для рендеринга, возвращаемых MyComponent, OtherComponent обёрнут в компонент React.Suspense, у которого есть свойство fallback. В нашем случае оказывается, что когда рендеринг доходит до OtherComponent, начинается загрузка соответствующего компонента. Тем временем рендерится то, что записано в свойство fallback. В данном примере это — текст Loading…. Вот, собственно говоря, и всё. Эти механизмы просто делают своё дело.

Правда, есть пара особенностей, которые нужно учитывать при работе с Lazy/Suspense.

  1. Компонент, который импортируется «ленивым» способом, должен содержать экспорт по умолчанию, который будет входной точкой компонента. Именованный экспорт тут использовать нельзя.
  2. Нужно оборачивать компонент, импортируемый с помощью функции React.lazy, в компонент React.Suspense. Компоненту React.Suspense нужно предоставить свойство fallback. В противном случае возникнет ошибка. Однако если вы попросту не хотите ничего рендерить до завершения ленивой загрузки компонента — вы можете просто записать в fallback значение null, не пытаясь каким-то хитрым способом обойти необходимость записи чего-то в этой свойство.

Помогло ли мне использование React Lazy/Suspense?


Да, помогло! Разделение кода сработало прямо-таки изумительно. Взглянем на результаты анализа нового бандла средствами Webpack.


Результаты работы Webpack Bundle Analyzer после разделения кода

Как видите, размер моего бандла уменьшился примерно на 50% — до 96 Кб. Отлично!

И что же, теперь проблема решена? Нет, к сожалению. Когда я взглянул на страницу, то оказалось, что у всплывающего окна с поздравлениями сбилось позиционирование.


Окно с поздравлениями выводится не там, где нужно

Проблема заключалась в том, что я «попросил» окно открыться, изменив состояние React-компонента. Между тем, я уже отрендерил null (то есть — ничего не отрендерил), используя компонент React.Suspense. После «ленивой» загрузки необходимых данных соответствующие материалы были добавлены в DOM. Однако позиционирование всплывающего окна уже было выполнено. В результате, из-за того, что свойства соответствующего компонента не менялись, этот компонент «не знал» о том, что ему нужно решить вопрос, касающийся позиционирования. Если я менял размер окна браузера, то всплывающее окно возникало в правильном месте из-за того, что соответствующий компонент наблюдал за изменениями свойств и за событиями изменения размеров окна, инициируя, если надо, повторное позиционирование.

Как же решить эту проблему? Решение заключалось в устранении «посредника».

Мне нужно было сначала загрузить «ленивый» компонент, а только тогда писать в состояние то, что сообщало бы окну с поздравлениями о том, что ему надо открыться. Я смог сделать это, используя те же механизмы Webpack по разделению кода, но теперь — без реализации импорта с помощью React.lazy:

async loadAndSetHappyMoment() {
  const component = await import(
    '../SidebarHappyMoment/SidebarHappyMoment.component'
  );
  this.SidebarHappyMoment = component.SidebarHappyMoment;
  this.setState({
    tooltipLevel: TooltipLevel.happyMoment,
  });
}

Эта функция вызывается после того, как моему компоненту, через механизм веб-сокетов, сообщется о том, что ему нужно показать окно с поздравлениями. Я воспользовался функцией Webpack import (вторая строчка кода). Если помните, то выше я говорил о том, что эта функция возвращает промис, в результате я смог воспользоваться конструкцией async/await.

После завершения загрузки компонента я записываю его в экземпляр моего компонента (командой this.SidebarHappyMoment = component.SidebarHappyMoment;). Это даёт мне возможность позже использовать его при рендеринге. Обратите внимание на то, что теперь я могу использовать именованные экспорты. В моём случае я воспользовался, в вышеупомянутой строчке, именем SidebarHappyMoment. И наконец, причём это не менее важно, чем всё остальное, я «сообщаю» окну о том, что ему нужно открыться, соответствующим образом меняя состояние уже после того, как я знаю о том, что компонент готов к работе.

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

renderTooltip() {
  if (this.state.tooltipLevel === TooltipLevel.happyMoment) {
    return <this.SidebarHappyMoment />;
  }
  //  ...
}

Обратите внимание на то, что команда return <this.SidebarHappyMoment />; возвращает this.SidebarHappyMoment — то, что я ранее записал в экземпляр моего компонента. Теперь это — нормальная синхронная render-функция, такая же, как те, которыми вы уже миллион раз пользовались. И теперь окно с поздравлениями выводится в точности там, где оно и должно выводиться. А всё дело в том, что оно открывается только после того, как его содержимое готово к использованию.

Продукт определяет архитектуру


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

Речь идёт о том, что продукт определяет то, что должно быть видимым и интерактивным тогда, когда компонент впервые рендерится. Это помогает разработчику разобраться в том, что именно он может отделить от основного кода и загрузить позже, когда в этом возникнет необходимость. Я поразмыслил над ситуацией и «вспомнил», что после того, как пользователь завершит выполнение рекомендованных шагов по настройке сайта, или в том случае, если он не является администратором сайта, мне вообще не нужно показывать ему прогресс-бар и то, что с ним связано. Теперь я, воспользовавшись этой информацией, смог продолжить разделение бандла. Вот что у меня получилось.


Продолжение разделения бандла

После этого размер бандла составлял всего 38 Кб. А мы, напомню, начинали со 190 Кб. Налицо уменьшение размера бандла на 80%. И я, кстати, уже вижу другие возможности по разделению кода. Мне не терпится продолжить оптимизацию бандла.

Итоги


Разработчики имеют свойство стремиться к тому, чтобы оставаться в своей «зоне комфорта» и не вникать ни во что кроме устройства самого кода и его функциональности. Однако программист, который пользуется вышеописанными инструментами, креативно мыслит и работает в тесной связи с другими специалистами, может получить возможность улучшения производительности своего приложения, значительно уменьшив размер бандла, содержащего то, что нужно для начала работы с этим приложением.

Уважаемые читатели! Пользуетесь ли вы разделением кода для повышения скорости загрузки своих веб-приложений?


  • +31
  • 5,8k
  • 6
RUVDS.com
1 513,37
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией

Комментарии 6

    +3
    Здесь использовался пакет lottie-web, что добавляло к размеру бандла 61.45 Кб.

    В дополнение к пакету lottie-web у меня был ещё и JSON-файл с описанием анимации. Размер этого файла составлял 26 Кб.

    Если кому-то нужно 26Кб для того, чтоб описать анимацию одного простенького компонента, а потом еще 62Кб, чтоб эти 26Кб начали что-то делать — имхо, проблема тут вовсе не в размерах зависимостей и отложенных загрузках.
      +2
      Согласен, для появления попапа можно было обойтись и без lottie и JSON'a)
      Но все же, в данной статье можно абстрагироваться именно от анимации (это просто пример), а сосредоточиться именно на использовании Webpack Analyzer и React Lazy/Suspense, думаю автор именно это и имел ввиду.
      +1
      Технологии React Lazy/Suspense приходят на помощь

      А уже можно нормально использовать в продакшене, или ещё подождать с пол годика? Интересен опыт, кто использовал.
        +1

        Использовал Lazy и Suspense, чтобы отделить загрузку графиков. А потом понадобилось сделать SSR.
        Suspense можно подключить с помощью react-async-ssr, а как вернуть lazy (который и позволяет разделять js файлы) так и не нашёл.

      –1
      Переходите на vue, там это реализовано.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое