Узнайте, как модули CommonJS влияют на tree shake вашего приложения
В этой статье мы рассмотрим, что такое CommonJS и почему он делает ваши пакеты JavaScript больше, чем необходимо.
Резюме: Чтобы гарантировать, что упаковщик сможет успешно оптимизировать ваше приложение, избегайте зависимости от модулей CommonJS и используйте синтаксис модулей ECMAScript во всем приложении.
Что такое CommonJS?
CommonJS — это стандарт 2009 года, который установил соглашения для модулей JavaScript. Первоначально он был предназначен для использования вне веб-браузера, в первую очередь для серверных приложений.
С помощью CommonJS вы можете определять модули, экспортировать из них функциональность и импортировать их в другие модули. Например, фрагмент ниже определяет модуль, который экспортирует пять функций: add
, subtract
, multiply
, divide
, and max
:
// utils.js
const { maxBy } = require('lodash-es');
const fns = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);
Позже другой модуль может импортировать и использовать некоторые или все эти функции:
// index.js
const { add } = require('./utils.js');
console.log(add(1, 2));
Вызов index.js
с помощью node
выведет в консоль число 3
.
Из-за отсутствия стандартизированной системы модулей в браузере в начале 2010-х годов CommonJS стал популярным форматом модулей и для клиентских библиотек JavaScript.
Как CommonJS влияет на размер вашего конечного пакета?
Размер вашего серверного приложения JavaScript не так критичен, как в браузере, поэтому CommonJS не был разработан с учетом уменьшения размера производственного пакета. В то же время анализ показывает, что размер пакета JavaScript по-прежнему является основной причиной замедления работы браузерных приложений.
Упаковщики и минификаторы JavaScript, такие как webpack
и terser
, выполняют различные оптимизации для уменьшения размера вашего приложения. Анализируя ваше приложение во время сборки, они пытаются удалить как можно больше из исходного кода, который вы не используете.
Например, в приведенном выше фрагменте ваш окончательный пакет должен включать только функцию add
поскольку это единственный символ из utils.js
, который вы импортируете в index.js
.
Давайте создадим приложение, используя следующую конфигурацию webpack
:
const path = require('path');
module.exports = {
entry: 'index.js',
output: {
filename: 'out.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'production',
};
Здесь мы указываем, что хотим использовать оптимизацию режима производства и использовать index.js
в качестве точки входа. После вызова webpack
, если мы исследуем размер вывода , мы увидим что-то вроде этого:
$ cd dist && ls -lah
625K Apr 13 13:04 out.js
Обратите внимание, что пакет весит 625 КБ . Если мы посмотрим на вывод, то найдем все функции из utils.js
, а также множество модулей из lodash
. Хотя мы не используем lodash
в index.js
он является частью вывода , что добавляет много дополнительного веса к нашим производственным активам.
Теперь давайте изменим формат модуля на модули ECMAScript и попробуем снова. На этот раз utils.js
будет выглядеть так:
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
import { maxBy } from 'lodash-es';
export const max = arr => maxBy(arr);
А index.js
будет импортировать из utils.js
, используя синтаксис модуля ECMAScript:
import { add } from './utils.js';
console.log(add(1, 2));
Используя ту же конфигурацию webpack
, мы можем построить наше приложение и открыть выходной файл. Теперь он составляет 40 байт со следующим выходом :
(()=>{"use strict";console.log(1+2)})();
Обратите внимание, что окончательный пакет не содержит ни одной из функций из utils.js
, которые мы не используем, и нет никаких следов от lodash
! Более того, terser
(JavaScript-минификатор, используемый webpack
) встроил функцию add
в console.log
.
Вы можете задать справедливый вопрос: почему использование CommonJS приводит к увеличению выходного пакета почти в 16 000 раз ? Конечно, это игрушечный пример, в реальности разница в размерах может быть не такой уж большой, но есть вероятность, что CommonJS значительно увеличит вес вашей производственной сборки.
Модули CommonJS сложнее оптимизировать в общем случае, поскольку они гораздо более динамичны, чем модули ES. Чтобы гарантировать, что ваш упаковщик и минификатор смогут успешно оптимизировать ваше приложение, избегайте зависимости от модулей CommonJS и используйте синтаксис модуля ECMAScript во всем вашем приложении.
Обратите внимание, что даже если вы используете модули ECMAScript в index.js
, если используемый вами модуль является модулем CommonJS, размер пакета вашего приложения пострадает.
Почему CommonJS увеличивает размер вашего приложения?
Чтобы ответить на этот вопрос, мы рассмотрим поведение ModuleConcatenationPlugin
в webpack
, а затем обсудим статическую анализируемость. Этот плагин объединяет область действия всех ваших модулей в одно замыкание и позволяет вашему коду выполняться быстрее в браузере. Давайте рассмотрим пример:
// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// index.js
import { add } from './utils.js';
const subtract = (a, b) => a - b;
console.log(add(1, 2));
Выше у нас есть модуль ECMAScript, который мы импортируем в index.js
. Мы также определяем функцию subtract
. Мы можем построить проект, используя ту же конфигурацию webpack
, что и выше, но на этот раз мы отключим минимизацию:
const path = require('path');
module.exports = {
entry: 'index.js',
output: {
filename: 'out.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
minimize: false
},
mode: 'production',
};
Давайте посмотрим на полученный результат:
/******/ (() => { // webpackBootstrap
/******/ "use strict";
// CONCATENATED MODULE: ./utils.js**
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// CONCATENATED MODULE: ./index.js**
const index_subtract = (a, b) => a - b;**
console.log(add(1, 2));**
/******/ })();
В выводе выше все функции находятся внутри одного пространства имен. Чтобы предотвратить коллизии, webpack переименовал функцию subtract
в index.js
в index_subtract
.
Если минификатор обработает исходный код, указанный выше, он:
- Удалить неиспользуемые функции
subtract
иindex_subtract
- Удалите все комментарии и лишние пробелы.
- Вставьте тело функции
add
в вызовconsole.log
Часто разработчики называют это удаление неиспользуемых импортов tree-shaking . Tree-shaking был возможен только потому, что webpack мог статически (во время сборки) понимать, какие символы мы импортируем из utils.js
и какие символы он экспортирует.
Такое поведение включено по умолчанию для модулей ES, поскольку они более поддаются статическому анализу по сравнению с CommonJS.
Давайте рассмотрим тот же самый пример, но на этот раз изменим utils.js
так, чтобы вместо модулей ES использовался CommonJS:
// utils.js
const { maxBy } = require('lodash-es');
const fns = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);
Это небольшое обновление значительно изменит вывод. Поскольку он слишком длинный для вставки на эту страницу, я поделился только небольшой его частью:
...
(() => {
"use strict";
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(288);
const subtract = (a, b) => a - b;
console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .add */ .IH)(1, 2));
})();
Обратите внимание, что окончательный пакет содержит некоторую "время выполнения" webpack
: внедренный код, который отвечает за импорт/экспорт функциональности из связанных модулей. На этот раз, вместо того, чтобы размещать все символы из utils.js
и index.js
в одном пространстве имен, мы динамически требуем, во время выполнения, функцию add
с помощью __webpack_require__
.
Это необходимо, поскольку с помощью CommonJS мы можем получить имя экспорта из произвольного выражения. Например, код ниже является абсолютно допустимой конструкцией:
module.exports[localStorage.getItem(Math.random())] = () => { … };
Упаковщик не может узнать имя экспортируемого символа во время сборки, поскольку для этого требуется информация, доступная только во время выполнения в контексте браузера пользователя.
Таким образом, минификатор не способен понять, что именно index.js
использует из своих зависимостей, поэтому он не может его вытряхнуть. Мы также увидим точно такое же поведение для сторонних модулей. Если мы импортируем модуль CommonJS из node_modules
, ваша сборочная цепочка инструментов не сможет оптимизировать его должным образом.
Tree-shaking с помощью CommonJS
Гораздо сложнее анализировать модули CommonJS, поскольку они динамические по определению. Например, местоположение импорта в модулях ES всегда является строковым литералом, в отличие от CommonJS, где это выражение.
В некоторых случаях, если используемая вами библиотека следует определенным соглашениям о том, как она использует CommonJS, можно удалить неиспользуемые экспорты во время сборки с помощью стороннего плагина webpack
. Хотя этот плагин добавляет поддержку tree-shaking, он не охватывает все различные способы, которыми ваши зависимости могут использовать CommonJS. Это означает, что вы не получаете тех же гарантий, что и с модулями ES. Кроме того, он добавляет дополнительные затраты как часть вашего процесса сборки сверх поведения webpack
по умолчанию.
Заключение
Чтобы гарантировать, что упаковщик сможет успешно оптимизировать ваше приложение, избегайте зависимости от модулей CommonJS и используйте синтаксис модулей ECMAScript во всем приложении.
Вот несколько действенных советов, которые помогут вам убедиться, что вы на оптимальном пути:
- Используйте плагин node-resolve Rollup.js и установите флаг
modulesOnly
, чтобы указать, что вы хотите зависеть только от модулей ECMAScript. - Используйте пакет
is-esm
, чтобы проверить, использует ли пакет npm модули ECMAScript. - Если вы используете Angular, по умолчанию вы получите предупреждение, если вы зависите от модулей, не поддерживающих tree-shake.