Spec-Zone.ru › webpack 4

Разделение кода

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

Разделение кода — одна из самых интересных функций webpack. Эта функция позволяет разделить ваш код на различные бандлы, которые затем можно загружать по запросу или параллельно. Это можно использовать для создания более компактных бандлов и управления приоритетом загрузки ресурсов, что при правильном использовании может существенно повлиять на время загрузки.

Существует три основных подхода к разделению кода:

  • Точки входа: ручное разделение кода с использованием конфигурации entry.
  • Предотвращение дублирования: использование плагина SplitChunksPlugin для удаления дублирования и разделения чанков.
  • Динамические импорты: разделение кода с помощью вызовов функций внутри модулей.

Точки входа

Это, безусловно, самый простой и интуитивно понятный способ разделения кода. Однако он более рутинный и имеет некоторые недостатки, которые мы рассмотрим. Давайте посмотрим, как мы можем разделить другой модуль из основного бандла:

project

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
+ |- another-module.js
|- /node_modules

another-module.js

import _ from 'lodash';

console.log(
  _.join(['Another', 'module', 'loaded!'], ' ')
);

webpack.config.js

const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
    index: './src/index.js',
+   another: './src/another-module.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

Это приведет к следующему результату сборки:

...
            Asset     Size   Chunks             Chunk Names
another.bundle.js  550 KiB  another  [emitted]  another
  index.bundle.js  550 KiB    index  [emitted]  index
Entrypoint index = index.bundle.js
Entrypoint another = another.bundle.js
...

Как упоминалось, этот подход имеет некоторые недостатки:

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

Первый из этих двух пунктов определенно является проблемой для нашего примера, так как lodash также импортируется в ./src/index.js и, следовательно, будет дублироваться в обоих бандлах. Давайте устраним это дублирование, используя плагин SplitChunksPlugin.

Предотвращение дублирования

Плагин SplitChunksPlugin позволяет нам извлечь общие зависимости в существующий чанк входа или в совершенно новый чанк. Давайте воспользуемся этим, чтобы удалить зависимость lodash из предыдущего примера:

Плагин CommonsChunkPlugin был удален в webpack v4 legato. Чтобы узнать, как обрабатываются чанки в последней версии, ознакомьтесь с SplitChunksPlugin.

webpack.config.js

  const path = require('path');

  module.exports = {
    mode: 'development',
    entry: {
      index: './src/index.js',
      another: './src/another-module.js',
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
+   optimization: {
+     splitChunks: {
+       chunks: 'all',
+     },
+   },
  };

С опцией конфигурации optimization.splitChunks, мы теперь должны увидеть, что дублирующаяся зависимость удалена из наших чанков index.bundle.js и another.bundle.js. Плагин должен заметить, что мы выделили lodash в отдельный чанк и удалили ненужные данные из основного бандла. Давайте выполним npm run build , чтобы посмотреть, сработало ли это:

...
                          Asset      Size                 Chunks             Chunk Names
              another.bundle.js  5.95 KiB                another  [emitted]  another
                index.bundle.js  5.89 KiB                  index  [emitted]  index
vendors~another~index.bundle.js   547 KiB  vendors~another~index  [emitted]  vendors~another~index
Entrypoint index = vendors~another~index.bundle.js index.bundle.js
Entrypoint another = vendors~another~index.bundle.js another.bundle.js
...

Вот некоторые другие полезные плагины и лоадеры, предоставленные сообществом для разделения кода:

  • mini-css-extract-plugin: Полезно для разделения CSS от основного приложения.
  • bundle-loader: Используется для разделения кода и отложенной загрузки получившихся бандлов.
  • promise-loader: Аналогично bundle-loader, но использует промисы.

Динамические импорты

Webpack поддерживает два похожих метода динамического разделения кода. Первый и рекомендуемый подход — использовать синтаксис import(), который соответствует предложению ECMAScript для динамических импортов. Старый, специфичный для webpack подход — использовать require.ensure. Давайте попробуем использовать первый из этих двух подходов...

Функции import() используют промисы внутри. Если вы используете import() со старыми браузерами, не забудьте подменить Promise с помощью полифила, например, es6-promise или promise-polyfill.

Прежде чем начать, давайте удалим дополнительные entry и optimization.splitChunks из нашей конфигурации, так как они не понадобятся для следующей демонстрации:

webpack.config.js

  const path = require('path');

  module.exports = {
    mode: 'development',
    entry: {
      index: './src/index.js',
-     another: './src/another-module.js',
    },
    output: {
      filename: '[name].bundle.js',
+     chunkFilename: '[name].bundle.js',
      publicPath: 'dist/',
      path: path.resolve(__dirname, 'dist'),
    },
-   optimization: {
-     splitChunks: {
-       chunks: 'all',
-     },
-   },
  };

Обратите внимание на использование chunkFilename, которое определяет имя файлов не-входных чанков. Для получения дополнительной информации о chunkFilename, см. документацию по выводу. Также обновим наш проект, чтобы удалить теперь неиспользуемые файлы:

project

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
- |- another-module.js
|- /node_modules

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

src/index.js

- import _ from 'lodash';
-
- function component() {
+ function getComponent() {
-   const element = document.createElement('div');
-
-   // Lodash, now imported by this script
-   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
+     const element = document.createElement('div');
+
+     element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+
+     return element;
+
+   }).catch(error => 'An error occurred while loading the component');
  }

- document.body.appendChild(component());
+ getComponent().then(component => {
+   document.body.appendChild(component);
+ })

Причина, по которой нам нужен default, заключается в том, что с webpack 4, при импорте модуля CommonJS, импорт больше не будет разрешаться в значение module.exports, а вместо этого будет создан искусственный объект пространства имен для модуля CommonJS. Для получения дополнительной информации о причинах этого, прочитайте webpack 4: import() и CommonJs

Обратите внимание на использование webpackChunkName в комментарии. Это приведет к тому, что наш отдельный бандл будет называться lodash.bundle.js вместо простого [id].bundle.js. Для получения дополнительной информации о webpackChunkName и других доступных опциях, см. import() документацию. Запустим webpack, чтобы увидеть, что lodash отделен в отдельный бандл:

...
                   Asset      Size          Chunks             Chunk Names
         index.bundle.js  7.88 KiB           index  [emitted]  index
vendors~lodash.bundle.js   547 KiB  vendors~lodash  [emitted]  vendors~lodash
Entrypoint index = index.bundle.js
...

Так как import() возвращает промис, он может быть использован с async функциями. Однако, для этого требуется препроцессор, например, Babel, и плагин Babel Syntax Dynamic Import. Вот как это упростило бы код:

src/index.js

- function getComponent() {
+ async function getComponent() {
-   return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
-     const element = document.createElement('div');
-
-     element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
-     return element;
-
-   }).catch(error => 'An error occurred while loading the component');
+   const element = document.createElement('div');
+   const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
+
+   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+
+   return element;
  }

  getComponent().then(component => {
    document.body.appendChild(component);
  });

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

Предварительная загрузка/Предварительная загрузка модулей

Webpack 4.6.0+ добавляет поддержку предварительной загрузки и предварительной загрузки.

Использование этих встроенных директив при объявлении ваших импортов позволяет webpack выводить «Resource Hint», который сообщает браузеру, что для:

  • prefetch: ресурс, вероятно, потребуется для какой-либо навигации в будущем
  • preload: ресурс может потребоваться во время текущей навигации

Простой пример предварительной загрузки может быть с компонентом HomePage, который рендерит компонент LoginButton, который затем по требованию загружает компонент LoginModal после нажатия.

LoginButton.js

//...
import(/* webpackPrefetch: true */ 'LoginModal');

Это приведет к тому, что <link rel="prefetch" href="login-modal-chunk.js"> будет добавлен в заголовок страницы, что даст указание браузеру предварительно загрузить файл login-modal-chunk.js в режиме ожидания.

Webpack добавит подсказку prefetch, как только родительский чанк будет загружен.

Директива preload имеет ряд отличий от prefetch:

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

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

Представьте компонент ChartComponent, который нуждается в большой ChartingLibrary. Он отображает LoadingIndicator при рендеринге и мгновенно выполняет импорт ChartingLibrary по требованию:

ChartComponent.js

//...
import(/* webpackPreload: true */ 'ChartingLibrary');

Когда запрашивается страница, использующая ChartComponent, чанк charting-library-chunk также запрашивается через <link rel="preload">. Предполагая, что чанк страницы меньше и загружается быстрее, страница будет отображена с LoadingIndicator, пока не завершится уже запрошенный charting-library-chunk. Это придаст небольшой импульс времени загрузки, так как потребуется только один запрос вместо двух. Особенно в средах с высокой задержкой.

Неправильное использование webpackPreload может навредить производительности, поэтому будьте осторожны при его использовании.

Анализ бандлов

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

  • webpack-chart: Интерактивная круговая диаграмма для статистики webpack.
  • webpack-visualizer: Визуализируйте и проанализируйте свои бандлы, чтобы увидеть, какие модули занимают много места и какие могут быть дубликатами.
  • webpack-bundle-analyzer: Плагин и утилита командной строки, представляющая содержимое бандла удобной интерактивной масштабируемой древовидной диаграммой.
  • webpack bundle optimize helper: Этот инструмент проанализирует ваш бандл и предоставит действенные рекомендации по улучшению для уменьшения размера бандла.
  • bundle-stats: Создайте отчет о бандле (размер бандла, активы, модули) и сравните результаты между различными сборками.

Дальнейшие шаги

См. Отложенную загрузку для более конкретного примера использования import() в реальном приложении и Кэширование, чтобы узнать, как более эффективно разделять код.

Дополнительное чтение

  • <link rel=”prefetch/preload”> в webpack
  • Preload, Prefetch И Приоритеты в Chrome
  • Предварительная загрузка контента с помощью rel="preload"

© JS Foundation and other contributors
Licensed under the Creative Commons Attribution License 4.0.
https://v4.webpack.js.org/guides/code-splitting

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API