Узнайте, как модули CommonJS влияют на структуру дерева вашего приложения.
В этом посте мы рассмотрим, что такое CommonJS и почему он делает ваши пакеты JavaScript больше, чем необходимо.
Резюме: Чтобы сборщик мог успешно оптимизировать ваше приложение, избегайте зависимости от модулей CommonJS и используйте синтаксис модулей ECMAScript во всем вашем приложении.
Что такое CommonJS?
CommonJS — это стандарт 2009 года, устанавливающий соглашения для модулей JavaScript. Изначально он предназначался для использования вне веб-браузера, в первую очередь для серверных приложений.
С помощью CommonJS вы можете определять модули, экспортировать из них функциональность и импортировать их в другие модули. Например, фрагмент ниже определяет модуль, который экспортирует пять функций: add
, subtract
, multiply
, divide
и 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));**
/******/ })();
В приведенном выше выводе все функции находятся в одном пространстве имен. Чтобы предотвратить коллизии, веб-пакет переименовал функцию subtract
в index.js
в index_subtract
.
Если минификатор обрабатывает приведенный выше исходный код, он:
- Удалите неиспользуемые функции
subtract
иindex_subtract
- Удалите все комментарии и лишние пробелы.
- Встроить тело функции
add
в вызовconsole.log
Часто разработчики называют такое удаление неиспользуемого импорта «тряской дерева» . Встряхивание дерева было возможно только потому, что веб-пакет мог статически (во время сборки) понимать, какие символы мы импортируем из utils.js
и какие символы он экспортирует.
Такое поведение включено по умолчанию для модулей ES , поскольку они более поддаются статическому анализу по сравнению с CommonJS.
Давайте посмотрим на тот же пример, но на этот раз изменим utils.js
, чтобы использовать CommonJS вместо модулей ES:
// 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
, ваша цепочка инструментов сборки не сможет его правильно оптимизировать.
Встряхивание дерева с помощью CommonJS
Анализировать модули CommonJS гораздо сложнее, поскольку они по определению динамичны. Например, местоположение импорта в модулях ES всегда является строковым литералом, в отличие от CommonJS, где это выражение.
В некоторых случаях, если используемая вами библиотека следует определенным соглашениям о том, как она использует CommonJS, можно удалить неиспользуемые экспорты во время сборки с помощью стороннего плагина webpack
. Хотя этот плагин добавляет поддержку встряхивания деревьев, он не охватывает все способы использования CommonJS вашими зависимостями. Это означает, что вы не получаете тех же гарантий, что и при использовании модулей ES. Кроме того, это добавляет дополнительные затраты в процессе сборки помимо поведения webpack
по умолчанию.
Заключение
Чтобы сборщик мог успешно оптимизировать ваше приложение, избегайте зависимости от модулей CommonJS и используйте синтаксис модуля ECMAScript во всем вашем приложении.
Вот несколько практических советов, которые помогут убедиться, что вы находитесь на оптимальном пути:
- Используйте плагин Node-Resolve Rollup.js и установите флаг
modulesOnly
, чтобы указать, что вы хотите зависеть только от модулей ECMAScript. - Используйте пакет
is-esm
, чтобы убедиться, что пакет npm использует модули ECMAScript. - Если вы используете Angular, по умолчанию вы получите предупреждение, если вы зависите от модулей, не поддерживающих дерево.