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

Мотивы

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

Мы не были исключением. С самого начала мы думали о важности оптимизированного процесса сборки, а также о четко реагирующей платформе (Ускорение процесса сборки на 500%, SSR в Zonky, Оптимизация БД, примечание: все написаны на чешском языке).

Однако после того, как мы недавно посмотрели анализ Lighthouse / ​​Page Speed ​​Insights *, мы поняли, что снова есть возможности для улучшения.

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

Для тех, кто не знаком со структурой пакета EmberJS: zonky-app.js - это файл, содержащий бизнес-логику, тогда как пакет vendor.js содержит код, связанный с фреймворком (сам фреймворк, дополнительные надстройки, сторонние библиотеки, не связанные с фреймворком, и т. Д.).

Что ж, Page Speed ​​Insights быстро помогла нам сузить круг вопросов, на которых нужно сосредоточиться дальше: код поставщика. На этом этапе мы могли бы поднять 3 проблемы, о которых нужно позаботиться:

  • Как легко определить влияние аддонов, входящих вvendor.js, на размер?
  • После обнаружения, что мы можем сделать, чтобы этого избежать?
  • Как быть уверенным, что добавление нового дополнения в будущем не вызовет большого увеличения размера пакета снова?

Анализ

Для анализа пакетов мы обратились к надстройке под названием ember-cli-bundle-analyzer. Он визуализирует весь пакет приложений в иерархическом порядке (сверху вниз), включая размеры его файлов.

Помимо размеров модулей, о которых мы знали, можно было заметить довольно большой кусок, представляющий библиотеку liphonenumber (в середине приложенного скриншота). Короче говоря, liphonenumber предоставляет более изощренный способ проверки телефонных номеров. Эта зависимость была добавлена ​​не так давно разработчиком, который не заметил размера зависимости при добавлении новой формы, позволяющей активировать учетную запись через SMS. Форма использовалась только на 1 странице, но вмещала ок. 80% модулей поставщика встроены в комплект приложений.

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

Реализация

Ленивая загрузка легко выполняется с помощью динамического импорта. Динамический импорт в настоящее время является функцией ECMA 3-го уровня. Работает аналогично статическому импорту, ​​но делает возможным следующее [1]:

  • импортировать модуль по запросу (или условно)
  • вычислить спецификатор модуля во время выполнения
  • импортировать модуль из обычного скрипта (в отличие от модуля).

Однако сообщество Ember не дремлет - внедрение этой функции уже доступно благодаря замечательному аддону под названием ember-auto-import (EAI). Аддон использует webpack за кулисами, чтобы убрать динамически загружаемые модули узлов из пакета приложений и при необходимости смонтировать их первыми.

Цель

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

// app/index/route.js
beforeModel() {
  ...
  return import('libphonenumber').then((module) => {
    // do sth with the loaded module
  });
},

Конфигурация

Что ж, давайте приведем пример того, как может выглядеть такая конфигурация для libphonenumber.

Измените ember-cli-build.js конфигурацию, чтобы надстройка EAI знала, какой модуль пропустить при объединении файлов поставщика.

autoImport: {
  publicAssetURL: '/assets',
  alias: {
    libphonenumber: 'google-libphonenumber/dist/libphonenumber',
  },
  // avoids multiple import
  exclude: ['qunit', 'moment', 'autosize', 'ldclient-js', 'rsvp'],
},

publicAssetsURL сообщает EAI, где вывести модуль в папке dist. Каждая такая зависимость будет (по умолчанию) отпечатана, получив имя с префиксом chunk (например, dist/assets/chunk.ed16d872df711b50f989.js).

Ключ alias обозначает ключ, который мы хотим использовать в нашем beforeModel() хуке для нацеливания на модуль. Значение указывает расположение модуля в папке node_modules. Если имя пакета совпадает с ключом, который вы хотите использовать, настраивать псевдоним не нужно.

Поскольку webpack и ember-cli могут избыточно связывать другие модули узлов, вам может потребоваться явно исключить их с помощью параметра конфигурации exclude.

Строительство

Во время процесса сборки (скажем, вы запускаете $ ember build -prod) EAI создает фрагмент для каждого модуля с отложенной загрузкой и помещает его в настроенный путь (в нашем случае это была папка dist/assets):

Некоторая информация, относящаяся к процессу объединения, отображается в консоли:

Asset                            Size       Chunks    Chunk Names
chunk.536b638f912d1a992d67.js  2.76 KiB       0       app
chunk.5d120ecc1871f382581f.js  25.7 KiB       2       vendors~app
chunk.8a81c3889ccbe62637dd.js  6.64 KiB       3       vendors~tests
chunk.a19df52fb071e2603fe8.js  1.91 KiB       1       tests
chunk.ed16d872df711b50f989.js   444 KiB       4       [big]
Entrypoint app = chunk.5d120ecc1871f382581f.js chunk.536b638f912d1a992d67.js
Entrypoint tests = chunk.8a81c3889ccbe62637dd.js chunk.a19df52fb071e2603fe8.js
...
[29] ./node_modules/google-libphonenumber/dist/libphonenumber.js 540 KiB {4} [built]
...
Built project successfully. Stored in "dist/".
File sizes:
- dist/assets/chunk.ed16d872df711b50f989.js: 444.26 KB (92.08 KB gzipped)
...

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

$ DEBUG="ember-auto-import:*" ember b -prod

Разъяснение

Теперь, если вы уже настроили привязку модели к import() модулю «на лету», вы сможете подтвердить правильное поведение на вкладке сети своего браузера:

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

Профилактика

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

Вместо того, чтобы писать собственный сценарий, мы обратились к надстройке под названием ember-cli-bundlesize. С супер простой конфигурацией, такой как:

'use strict';
module.exports = {
  'js-app': {
    pattern: 'assets/zonky-app-*.js',
    limit: '200KB',
    compression: 'gzip',
  },
  'js-vendor': {
    pattern: 'assets/vendor-*.js',
    limit: '500KB',
    compression: 'gzip',
  },
  css: {
    pattern: 'assets/styles/*.css',
    limit: '50KB',
    compression: 'gzip',
  },
};

Это оказалось именно то, что нам нужно, потому что:

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

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

В случае, если этот размер остается в установленных пределах, запуск ember bundlesize:test хвалит вас за хорошую работу:

Резюме

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

Этот относящийся к EAI код был добавлен, однако в будущем он не будет сильно разрастаться в следующих модулях. Следовательно, мы добавили больше зависимостей с отложенной загрузкой в ​​наш список ToDo и убеждены, что с новыми требованиями это станет еще лучше; Например, другой язык для поддержки (в настоящее время 12+ КБ). Уже появляются PR, использующие этот подход с загрузкой по требованию, поскольку они добавляют функциональность к приложению только в случае мобильных устройств или других конкретных случаев, в которых общая кодовая база не обязательно нужна.

* продемонстрированный анализ был проведен Lighthouse в Chrome с локальным запуском приложения со следующими настройками:

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

Источники:

[1] Динамический импорт, Матиас Биненс https://developers.google.com/web/updates/2017/11/dynamic-import