Повысьте производительность SPA, разбив ваши библиотеки Angular на несколько частей

Привет, Хабр! Представляю Вашему вниманию перевод статьи «Improve SPA performance by splitting your Angular libraries in multiple chunks» автора Kevin Kreuzer.


Angular — отличный фреймворк. Мы все его любим <3.


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


Сегодня благодаря Angular CLI библиотеки легко создать. Они прекрасно подходят для того, чтобы делиться кодом между несколькими приложениями.


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


В Frontend есть разные типы производительности. runtime — производительность и initial load. В этой статье мы сосредоточимся на initial load.


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


"Это такая простая библиотека. Она не может повлиять на производительность, верно?"


Давайте начнем с простой библиотеки, которую мы создадим при помощи Angular CLI. Если вы никогда не создавали библиотеку Angular, для вас может быть полезно прочитать следующую статью:


image


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


Библиотека называется howdy и её единственная цель поприветствовать вас Вашим именем или сказать вам местное время. В ней содержится два модуля с каждым компонентом. Один модель приветствует, другой говорить время.


image


Просто обычный module Angular и Component, который принимает свойство name поверх привязок Input и отображает его.


image


HowdyTimeComponent отвечает за отображение времени с помощью сторонней библиотеки moment.


Отлично! Наша библиотека howdy готова к публикации! Это такая простая библиотека; она не сможет повлиять на производительность, верно?


Потребление библиотеки howdy


Теперь у нас есть библиотека howdy! Было бы стыдно не воспользоваться этим. Чтобы использовать библиотеку howdy, мы создаем новый SPA с Angular CLI.


ng new greeting-app

Поскольку нас интересует производительность, давайте также установим зависимость dev, которая называется webpack-bundle-analyzer.


npm i -D webpack-bundle-analyzer

Webpack-bundle-analyzer позволяет вам визуализировать размер выходных файлов webpack с помощью интерактивной масштабируемой древовидной карты.


Лучший способ проанализировать наш пакет — добавить следующий скрипт анализа в наш package.json.


"analyze": "ng build --prod --stats-json && webpack-bundle-analyzer ./dist/greeting-app/stats-es2015.json"

Если мы запустим эту команду, Angular выполнит производственную сборку, а также выведет stats-es2015.json, который затем будет выбран и визуализирован webpack-bundle-anlyzer.


image


Поскольку мы еще не написали никакого кода, наш основной пакет в основном состоит из Angular. Мы также можем видеть, что zone.js включен в наш пакет polyfill.


В целом размер нашего приложения теперь составляет 207 КБ.


Но мы еще не включили нашу Howdy библиотеку! Давайте продолжим и сделаем это.


npm i howdy

Мы установили howdy библиотеку, потому что мы хотим разместить приветствие с именем. Нас не интересует демонстрация времени. Поэтому мы будем использовать только модуль HowdyNameModule и не будем включать HowdyTimeModule.


image


Здесь важно отметить, что мы импортируем только HowdyNameModule. Давайте снова запустим скрипт анализа analyze.


image


Ого! Довольно круто! Мы перешли с 207 КБ до 511.15 КБ. Размер увеличился больше чем в два раза. Какого…!


Одного взгляда достаточно, чтобы найти виновника. moment огромный! Он приносит свой основной код реализации и все региональные настройки.


Конечно, moment может быть заменен другими пакетами, такими как date-fns или moment-mini. Но вопрос стоит в другом; Почему он вообще там есть? Помните, что мы только импортировали только HowdyNameModule, а не HodwyTimeModule. Я думал, что когда происходит Tree shaking, стряхиваются только неиспользуемые модули? Что происходит?


Tree shaking может убрать не все


Чтобы Tree shaking произошел, сборка Angular запускает кучу расширенных оптимизаций. Но все равно moment присутствует в комплекте, хотя HowdyTimeModule нет.
Проблема заключается в том, как moment упакован. Давайте быстро взглянем на файл moment.js в нашей папке node_modules.


image


Так как moment может быть использован во множестве мест, как и бэкенды Node JS, приложения Angular или обыкновенный JavaScript, он связан в UMD, а не как модуль ES.


Связанные библиотеки UMD обернуты в функцию IFFE, что означает, что ModuleConcatenation не может быть использован.Инструменты оптимизации сборки никак не могут узнать будет ли этот код использован, или у него есть побочные эффекты.


В двух словах, этот тип модуля не позволяет Angular запускать более продвинутый комплект оптимизации.


К сожалению, мы не можем контролировать, как moment комплектуется. Означает ли это, что мы должны смириться с огромным размером пакета?


Вторичные точки входа для победы


Мы не можем контролировать, как создается moment. Но мы можем управлять нашей библиотекой. И действительно, есть способ предотвратить такие сценарии. Вторичные точки входа!


В настоящее время почти все библиотеки Angular упакованы с помощью ng-packagr. ng-packagr позволяет вам использовать ng-package.json в сочетании с public-api, который в конечном итоге станет точкой входа в ваше приложение.


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


Звучит неплохо! Как активировать вторичные точки входа?


Вторичные точки входа динамически обнаруживаются с помощью ng-packagr. ng-packagr ищет файлы package.json в подкаталогах основной папки файла package.json


Круто! Давайте воспользуемся преимуществом вторичных точек входа в нашей библиотеке howdy, добавив туда следующие файлы.


image


Для каждого модуля мы добавили index.ts, package.json и public_api.ts.


  • index.ts находится там, только чтобы указывать на public_api, который полезен во время импорта.
  • public_api экспортирует все модули и компоненты из нашего модуля.
  • package.json содержит особые конфигурации ng-packagr. В нашем случае этого достаточно, чтобы конкретизировать entryFile.

В package.json также могут содержаться другие свойства, такие как cssUrl и т. д. Обратите внимание, что область действия этих свойств — только текущая подстатья.

Если мы сейчас запустим сборку, мы получим три блока. howdy.js, howdy-src-lib-name.js и howdy-src-lib-time.js.


Howdy-src-lib-name.js теперь содержит только код, относящийся к HowdyNameModule, а howdy-src-lib-time.js теперь содержит только код, специфичный для HowdyTimeModule.


Но давайте посмотрим на кусок howdy.js.


image


Кусок howdy.js все еще содержит HowdyNameComponent и HowdyTimeComponent. Это означает, что мы все равно получим moment даже если импортируем только HowdyNameModule.


Если мы хотим избавиться от HowdyTimeModule с помощью этого подхода, нам нужно использовать глубокий импорт. Так что мы импортируем не из howdy.js, а напрямую из howdy-src-lib-time.js
Что не рекомендуется! Глубокий импорт опасен, и его всегда следует избегать!

Как мы можем решить эти проблемы? Как мы можем гарантировать, что HowdyTimeModule также будет убран, даже если мы используем стандартный импорт? Что ж, нам нужно настроить способ создания куском howdy.js.


Используйте “signpost”


Идея состоит в том, чтобы удалить код из блока howdy.js и вместо этого позволить ему действовать как своего рода “signpost”«указатель», который указывает вам на другие блоки.


Поэтому давайте подробнее рассмотрим src / public_api.ts.


/*
 * Public API Surface of howdy
 */
export * from './lib/name/howdy-name.component';
export * from './lib/name/howdy-name.module';
export * from './lib/time/howdy-time.component';
export * from './lib/time/howdy-time.module';

Эти строки отвечают за включение всего, от name и time, в блок howdy.js. Нам нужно удалить код из howdydy.js и позволить ему указывать на другие фрагменты, которые содержат реализацию. Давайте изменим его содержание.


/
 * Public API Surface of howdy
 */
export * from 'howdy/src/lib/name';
export * from 'howdy/src/lib/time';

Вместо того, чтобы экспортировать реальную реализацию, мы указываем относительный путь к различным частям. С этим изменением howdy.js указывает только на другие пакеты и не содержит никакого «реального» кода.
Давайте запустим ng build и проанализируем нашу папку dist.


image


Howdy.js теперь действует как “signpost”«указатель», который указывает на фрагменты, содержащие реализацию. В блоке howdy-src-lib-name.js содержится только код из папки name, а в файле howdy-src-lib-time.js содержится только код из папки time.


Завершить пакет с подстатьями


Давайте обновим пакет howdy в нашем приветственном приложении и повторно запустим скрипт анализа.


image


Круто. Размер пакета теперь составляет 170,94 КБ. Чуть выше, чем изначально. Давайте посмотрим, как выглядит модуль Howdy в финальном комплекте.


image


Замечательно! Эта корректировка позволяет нам сохранить размер пакета потребляющего SPA небольшим. SPA получает только то, что им нужно!


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

Реальный опыт


Приведенный выше пример очень прост.


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


Скорее всего, вам понадобится настроить некоторые пути импорта или конкретизировать некоторые пути в вашем файле tsconfig.json. И вы также столкнетесь с модулями из одного блока, которые используют модули из другого блока.


Но поверьте мне, как только вы справитесь с этим бременем, оно того стоит.


В обширной корпоративной среде мы выяснили, что размер пакета недавно созданных SPA взорвался после того, как мы включили некоторые из предоставленных нами библиотек.


В какой-то момент он даже поднялся до 5 МБ. Каждый SPA получил moment, @swimlane / datatable и другие вещи, которые он даже не использовал. Мы начали фокусироваться на оптимизации этого размера пакета.


Мы убрали moment с date-fns и начали использовать вторичные точки входа. В настоящее время мы получили основной блок размером 662 КБ для вновь созданного SPA, который включает в себя несколько библиотек. Это все еще много, но мы еще не закончили. Оптимизация еще не завершена — мы можем уменьшить размер пакета еще больше.


Очень круто видеть, где мы сейчас и откуда пришли.


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


Заключение


Angular делает замечательную работу, когда речь идет об оптимизации размера пакета. Хотя шаги сборки по оптимизации очень сложны, они не могут стрясти “tree shaking” все.


Модули, упакованные в других форматах, кроме ESModules, не могут быть tree shaked.


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


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

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

    +1

    Хорошая статья, спасибо.

      0

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

        0
        Бывают ситуации, когда необходимо управлять загрузкой библиотеки runtime. Скажем сторонняя библиотека весит 1Mb. Отображение компонента, использующее эту библиотеку, покрыто *ngIf. У вас есть какой-нибудь опыт решения данной ситуации?
          0
          По-моему как-то слишком сложно создавать для каждого модуля index.ts, package.json и public_api.ts. Взять, к примеру, ng-bootstrap, там всего один index.ts.
            0
            Если разрабатывает один человек да, все верно. Если участвует больше то болен оправдано. Особенно если предпалагается расширение команды и возможный распил репы на множество реп. Меньше переделывать придется
              0
              Меньше переделывать придется
              Зато изначально больше работы. В итоге — никакой экономии времени. Надо считать общее время, а не только по одной задаче (в данном случае «распил репы на множество реп»).
                0
                В малых и средних самостоятельных проектах полностью верно. Когда речь идет о больших проектах, модульная независимость важнее максимального быстрого получения результата. Разбиение проекта сразу на множество репозиторий не эффективно. Но можно разбить на модули по проектам. Т.е. система контроля кода общая, но разработчики работают в отдельных проектах, что соответсвует отдельным package.json. Возможно автору стоило рассмотреть вариант использования одного package.json, но настроив ts разные области видимости. Можно много дискутировать, все зависит от размера как проекта, так и комманды )

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

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