Дедупликация модулей — это процесс устранения повторяющихся зависимостей внутри итогового бандла. В контексте Webpack под дублированием понимается ситуация, когда один и тот же модуль, библиотека или набор функций попадает в сборку несколько раз.
Подобные дубликаты возникают при:
splitChunks;Основная цель дедупликации — уменьшение размера бандла и снижение объёма выполняемого JavaScript-кода.
Наиболее распространённая ситуация:
npm ls lodash
Результат:
project
├── lodash@4.17.21
├─┬ package-a
│ └── lodash@4.17.20
└─┬ package-b
└── lodash@4.17.21
Webpack воспринимает разные версии как разные модули.
В результате:
lodash@4.17.20;lodash@4.17.21;Даже одинаковые файлы могут считаться разными модулями.
Пример:
import utils from './utils';
import utils2 from './utils/index.js';
Если конфигурация настроена неправильно, Webpack может создать два независимых модуля.
Вложенные зависимости часто создают копии библиотек:
node_modules/
react/
package-a/
node_modules/
react/
В этом случае в бандле окажутся две копии React.
Это особенно критично для:
Некоторые библиотеки вообще не способны корректно работать в двух экземплярах.
Webpack строит граф зависимостей.
Каждый модуль получает внутренний идентификатор на основе:
Например:
/src/utils/index.js
и
/src/utils/index.js?raw
для Webpack — разные модули.
Webpack автоматически устраняет часть дубликатов.
Если один и тот же путь импортируется многократно:
import a from './math';
import b from './math';
Webpack создаёт только один экземпляр модуля.
Это базовый механизм кэширования внутри dependency graph.
Во время выполнения бандла используется runtime-кэш:
__webpack_module_cache__
После первого выполнения модуля результат сохраняется.
Повторные импорты используют уже созданный экземпляр.
Это предотвращает:
Наиболее важный инструмент дедупликации —
SplitChunksPlugin.
Допустим, имеются два entry:
entry: {
admin: './src/admin.js',
client: './src/client.js'
}
Оба используют React:
import React from 'react';
Без оптимизации React может попасть:
admin.bundle.js;client.bundle.js.Конфигурация:
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
Webpack анализирует граф зависимостей и выносит общие модули в отдельный chunk.
Результат:
vendors.js
admin.js
client.js
Теперь React находится только в vendors.js.
Webpack анализирует:
node_modules;После этого принимается решение:
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
Все зависимости из node_modules собираются отдельно.
optimization: {
splitChunks: {
cacheGroups: {
common: {
minChunks: 2,
minSize: 0,
name: 'common',
chunks: 'all'
}
}
}
}
Модули, используемые минимум два раза, выносятся в общий chunk.
Ключевой механизм дедупликации.
minChunks: 2
Означает:
модуль должен использоваться минимум в двух чанках.
Пример:
Button.js
используется:
home.js;profile.js.Webpack создаст:
common.js
и вынесет Button.js туда.
Webpack runtime тоже может дублироваться.
Каждый entry содержит:
При большом количестве entry runtime начинает занимать заметный объём.
Решение:
optimization: {
runtimeChunk: 'single'
}
Webpack создаёт отдельный runtime:
runtime.js
vendors.js
app.js
admin.js
Теперь bootstrap-код не дублируется.
В микрофронтендах проблема становится особенно серьёзной.
Например:
Без дедупликации браузер загрузит две копии.
Module Federation предоставляет механизм shared modules.
shared: {
react: {
singleton: true
},
'react-dom': {
singleton: true
}
}
singleton: true
означает:
использовать только один экземпляр библиотеки.
Это критически важно для React.
shared: {
react: {
eager: true
}
}
Библиотека загружается сразу.
Плюсы:
Минусы:
По умолчанию shared-зависимости могут загружаться динамически.
Это снижает initial bundle size.
Иногда разные пакеты используют разные пути к одной библиотеке.
resolve: {
alias: {
react: path.resolve(__dirname, 'node_modules/react')
}
}
Теперь все импорты React указывают на единый экземпляр.
В monorepo и локальных пакетах часто возникают дубликаты.
Причина:
resolve: {
symlinks: false
}
Webpack перестаёт разворачивать симлинки.
Это помогает избежать появления нескольких копий одной библиотеки.
Webpack не способен полностью устранить дубликаты, если они уже
существуют в node_modules.
Команда:
npm dedupe
пытается поднять одинаковые зависимости выше по дереву.
Пример:
До:
package-a/node_modules/react
package-b/node_modules/react
После:
node_modules/react
Принудительное выравнивание версий:
{
"resolutions": {
"react": "^18.2.0"
}
}
Все зависимости будут использовать единую версию.
В pnpm используется контентно-адресуемое хранилище.
Это уже уменьшает физическое дублирование файлов.
Дополнительно:
pnpm dedupe
выравнивает версии зависимостей.
Tree Shaking и дедупликация — разные механизмы.
Удаляет:
Удаляет:
CommonJS хуже поддаётся дедупликации.
Причины:
Пример:
require(moduleName);
Webpack не может заранее определить зависимость.
ESM намного эффективнее.
Webpack способен:
ModuleConcatenationPlugin уменьшает overhead между
модулями.
optimization: {
concatenateModules: true
}
Webpack объединяет несколько модулей в одну функцию.
Это снижает:
Установка:
npm install webpack-bundle-analyzer --save-dev
Подключение:
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
plugins: [
new BundleAnalyzerPlugin()
]
Инструмент позволяет увидеть:
Признаки:
Например:
lodash
date-fns
moment
core-js
Несколько версий сильно раздувают vendors chunk.
Часто возникает при:
Webpack осторожно относится к модулям с побочными эффектами.
{
"sideEffects": false
}
Это позволяет:
Иногда устранение дубликатов приводит к ошибкам.
Пример:
react@17
react@18
Принудительное объединение может сломать приложение.
Некоторые библиотеки зависят от singleton-состояния:
Две копии создают конфликт контекстов.
Слишком агрессивный splitChunks способен:
Огромный общий chunk — не всегда хорошо.
vendors.js = 4 MB
Изменение одной библиотеки инвалидирует весь кэш.
Разделение по группам:
cacheGroups: {
reactVendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor'
},
utilityVendor: {
test: /[\\/]node_modules[\\/](lodash|date-fns)[\\/]/,
name: 'utility-vendor'
}
}
Это улучшает:
Webpack 5 использует:
optimization: {
moduleIds: 'deterministic'
}
Это уменьшает churn hash-значений между сборками.
Влияние на дедупликацию косвенное, но важное:
Webpack умеет удалять модули, уже доступные в родительских чанках.
Механизм:
Особенно эффективно при dynamic imports.
Пример:
import('./dashboard');
import('./profile');
Оба чанка используют Chart.js.
Webpack может:
Поведение зависит от:
splitChunks.chunks;minSize;minChunks;reuseExistingChunk.reuseExistingChunk: true
Webpack переиспользует уже созданный chunk вместо генерации нового.
Это уменьшает duplication между async-модулями.
module.exports = {
optimization: {
runtimeChunk: 'single',
moduleIds: 'deterministic',
splitChunks: {
chunks: 'all',
cacheGroups: {
reactVendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor',
priority: 20
},
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
Грамотно настроенная дедупликация позволяет: