ModuleConcatenationPlugin — встроенный плагин Webpack,
реализующий механизм scope hoisting. Его задача заключается в
объединении нескольких JavaScript-модулей в одну общую область видимости
без создания дополнительных обёрток функций для каждого модуля.
До появления scope hoisting большинство сборщиков формировали итоговый bundle по схеме:
[
function(module, exports, require) {
// module A
},
function(module, exports, require) {
// module B
}
]
Каждый модуль изолировался собственной функцией. Такая архитектура удобна, но создаёт дополнительные накладные расходы:
ModuleConcatenationPlugin устраняет часть этих проблем,
объединяя совместимые модули:
// module A
const value = 10;
// module B
console.log(value);
После конкатенации:
const value = 10;
console.log(value);
Webpack перестаёт создавать отдельные runtime-обёртки для некоторых модулей и превращает их в единый фрагмент кода.
Термин scope hoisting появился благодаря Rollup, где такая оптимизация была реализована одной из первых.
Смысл подхода:
Без scope hoisting:
(function() {
function moduleA() {
return 5;
}
function moduleB() {
console.log(moduleA());
}
moduleB();
})();
Со scope hoisting:
function moduleA() {
return 5;
}
console.log(moduleA());
Удаляются лишние уровни абстракции.
В production-режиме Webpack автоматически включает
ModuleConcatenationPlugin.
Эквивалент:
module.exports = {
mode: 'production'
};
Webpack internally добавляет:
new webpack.optimize.ModuleConcatenationPlugin()
В development-режиме плагин отключён.
Явное подключение:
const webpack = require('webpack');
module.exports = {
mode: 'none',
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
Обычно ручное подключение требуется:
mode: 'none';Webpack строит dependency graph и анализирует:
После анализа формируются группы модулей:
Пример:
// math.js
export const sum = (a, b) => a + b;
// app.js
import { sum } from './math';
console.log(sum(1, 2));
Без конкатенации:
__webpack_require__(/*! ./math */);
С конкатенацией:
const sum = (a, b) => a + b;
console.log(sum(1, 2));
Удаляются:
__webpack_require__;Разница особенно заметна при большом количестве мелких модулей.
JavaScript-движок выполняет меньше:
Для крупных SPA это может заметно снижать время запуска.
После объединения модулей Webpack проще анализировать использование экспортов.
Пример:
export function used() {}
export function unused() {}
Если код объединён в одну область видимости, неиспользуемые части проще удалить во время minimization.
V8 хуже оптимизирует deeply nested closures.
Scope hoisting позволяет:
Несмотря на преимущества, Webpack не может объединять все модули подряд.
Существует большое количество ограничений.
Конкатенация работает только для ES-модулей.
Поддерживается:
import { x } from './a';
export const value = 1;
Не поддерживается:
const lib = require('./a');
module.exports = {};
CommonJS несовместим со scope hoisting, потому что:
Пример динамического require:
require('./' + name);
Webpack не способен заранее определить:
Из-за этого модуль исключается из конкатенации.
Модули с import() создают отдельные chunk.
Пример:
import('./admin');
Такие зависимости становятся асинхронными границами.
Webpack не может конкатенировать:
Если модуль используется в нескольких chunk одновременно, конкатенация часто становится невозможной.
Причина:
Пример:
entry: {
main: './src/main.js',
admin: './src/admin.js'
}
Общий utility-модуль может быть вынесен в отдельный chunk вместо конкатенации.
Hot Module Replacement плохо сочетается со scope hoisting.
Во время HMR Webpack должен:
После конкатенации границы размываются.
Поэтому в development:
Webpack избегает агрессивной конкатенации модулей с потенциальными side effects.
Пример:
console.log('init');
export const value = 1;
или:
window.shared = {};
Такие модули могут влиять на глобальное состояние.
Webpack обязан сохранить корректный порядок исполнения.
Для эффективной оптимизации используется:
{
"sideEffects": false
}
Либо:
{
"sideEffects": [
"*.css"
]
}
Это помогает Webpack:
Сложные цепочки re-export ухудшают конкатенацию.
Пример:
export * from './a';
export * from './b';
Особенно проблемны:
Конструкция:
import * as utils from './utils';
создаёт namespace object.
Webpack сложнее анализировать подобные структуры.
Иногда это блокирует оптимизацию.
Некоторые devtool-режимы несовместимы со scope hoisting.
Особенно:
devtool: 'eval'
или:
devtool: 'eval-source-map'
Webpack вынужден генерировать код в виде отдельных eval-блоков, что мешает объединению модулей.
Проблемный пример:
for (const key in exports) {
console.log(key);
}
или:
Object.assign(exports, dynamicData);
Webpack теряет возможность статически анализировать структуру экспортов.
Циклические зависимости значительно усложняют scope hoisting.
Пример:
// a.js
import { b } from './b';
export const a = 1;
// b.js
import { a } from './a';
export const b = 2;
Webpack может отказаться от конкатенации для сохранения корректного порядка инициализации.
ES Modules имеют:
this === undefined
CommonJS:
this === module.exports
Если модуль использует top-level this, Webpack часто
исключает его из конкатенации.
ESM всегда strict mode.
Некоторые legacy-модули:
Это создаёт несовместимость.
Одна из самых распространённых проблем — неправильная настройка Babel.
Плохая конфигурация:
{
presets: [
['@babel/preset-env', {
modules: 'commonjs'
}]
]
}
Babel превращает ESM в CommonJS:
import x from './x';
становится:
var x = require('./x');
Webpack больше не может выполнять scope hoisting.
Нужно сохранять ES-модули:
{
presets: [
['@babel/preset-env', {
modules: false
}]
]
}
или:
{
presets: ['@babel/preset-env']
}
при использовании Babel вместе с Webpack.
Webpack помечает объединённые модули.
Статистика:
webpack --display-optimization-bailout
или:
stats: {
optimizationBailout: true
}
Webpack показывает причины отказа.
Примеры:
ModuleConcatenation bailout:
Module is not an ECMAScript module
или:
Cannot concat with ./node_modules/library/index.js
или:
Module uses eval()
Эти сообщения критически важны при оптимизации bundle.
Генерация:
webpack --json > stats.json
Далее анализируются:
После объединения модулей source maps становятся сложнее.
Проблемы:
Особенно заметно в крупных проектах.
Scope hoisting ухудшает:
В development это неудобно.
Поэтому production и development-конфигурации обычно различаются.
Наибольший эффект достигается совместно с minimizer.
После конкатенации Terser способен:
До конкатенации:
function moduleA(exports) {
exports.value = 5;
}
function moduleB(require) {
console.log(require('./a').value);
}
После конкатенации и minimization:
console.log(5);
Эти механизмы тесно связаны.
Tree shaking удаляет:
Scope hoisting упрощает анализ зависимостей между модулями.
Вместе они формируют основу production-оптимизации Webpack.
Эффект минимален если:
Наиболее заметная выгода наблюдается:
Webpack 5 улучшил:
Но фундаментальные ограничения scope hoisting сохранились:
Предпочтительно:
import/export
вместо:
require/module.exports
Избыточные:
export * from ...
ухудшают анализ зависимостей.
Критически важно:
modules: false
Неверная настройка:
{
"sideEffects": false
}
может привести к удалению нужного кода.
Типичная схема:
mode: 'development'
без aggressive optimization.
И:
mode: 'production'
со scope hoisting и minimization.
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
optimization: {
concatenateModules: true,
usedExports: true,
minimize: true
}
};
В Webpack 5 существует shorthand-опция:
optimization: {
concatenateModules: true
}
Она управляет ModuleConcatenationPlugin internally.
Отключение:
optimization: {
concatenateModules: false
}
Полезно: