Tree Shaking в Webpack основан на статическом анализе импортов и экспортов. Его эффективность напрямую зависит от формата модулей. Наиболее критичная проблема возникает при использовании CommonJS, поскольку он изначально не проектировался как статически анализируемая система модулей.
CommonJS использует динамическую природу require, а
также изменяемый объект module.exports, что делает
невозможным точное определение используемых экспортов на этапе
сборки.
В CommonJS импортирование выполняется через функцию:
const lib = require('./lib');
Проблема заключается в том, что require может вызываться
в любом месте кода, включая:
if (condition) {
const mod = require('./module');
}
Такой подход разрушает возможность построения статического графа зависимостей. Webpack не может заранее определить, будет ли модуль загружен и какие его части реально используются.
В CommonJS экспорт представляет собой обычный объект:
module.exports = {
a: 1,
b: 2
};
или его мутация:
exports.a = 1;
exports.b = 2;
Проблема Tree Shaking заключается в том, что объект можно изменять динамически:
module.exports.a = condition ? value1 : value2;
или даже перезаписывать:
module.exports = getExports();
Такая гибкость делает невозможным гарантированное определение «чистого» набора экспортов.
Tree Shaking эффективно работает, когда есть независимые именованные экспорты:
export function a() {}
export function b() {}
В CommonJS всё упаковано в один объект. Даже если используется только одно свойство, на уровне модуля остаётся зависимость от всего объекта:
const { a } = require('./lib');
На практике Webpack часто вынужден включать весь модуль, поскольку анализ деструктуризации в CommonJS не всегда безопасен. Особенно если объект формируется динамически.
Даже если исходный код написан в формате ES Modules, транспиляция через Babel может преобразовать его в CommonJS:
export const a = 1;
после трансформации:
exports.a = 1;
В результате Tree Shaking полностью деградирует, поскольку Webpack теряет возможность использовать ESM-граф зависимостей.
Критический момент — preset @babel/preset-env при
неправильной конфигурации modules: "commonjs".
CommonJS-модули часто содержат побочные эффекты при загрузке:
console.log('module loaded');
module.exports = function() {};
Даже если экспорт не используется, модуль нельзя безопасно удалить, потому что:
Webpack учитывает это как потенциальный side effect и оставляет модуль в бандле.
Многие npm-пакеты одновременно используют CommonJS и ES Module синтаксис. Это создаёт дополнительные сложности:
default export__esModule)Пример:
module.exports = require('./lib');
module.exports.default = module.exports;
Webpack вынужден добавлять слой совместимости, что ухудшает возможность точного Tree Shaking.
Частая практика — использование индексных файлов:
export * from './a';
export * from './b';
export * from './c';
В CommonJS эквиваленте:
module.exports = {
...require('./a'),
...require('./b'),
...require('./c')
};
Проблема в том, что агрегация через spread или ручное объединение разрушает связь между конкретным экспортом и его источником. Webpack не может точно удалить неиспользуемые части.
Особенно проблемными являются конструкции:
require(`./modules/${name}`);
или:
const mod = require(paths[i]);
Webpack превращает такие случаи в require.context, что
приводит к включению потенциально всех модулей директории. Tree Shaking
в таком случае становится неприменимым, поскольку границы зависимости
неопределены.
Webpack использует флаг:
optimization.usedExportsОн позволяет помечать используемые экспорты, но в CommonJS его эффективность ограничена:
В результате Webpack часто помечает модуль как «используемый целиком».
Даже при невозможности полноценного Tree Shaking часть мёртвого кода может быть удалена на этапе минификации (Terser):
Однако это работает только на уровне синтаксического анализа AST и не заменяет модульный Tree Shaking.
Несмотря на ограничения, Webpack способен выполнять ограниченную оптимизацию, если соблюдаются условия:
requiremodule.exports фиксированаПример более «дружелюбного» CommonJS:
exports.a = function a() {};
exports.b = function b() {};
Даже в этом случае анализ остаётся менее точным, чем в ES Modules.
Ключевое различие:
ES Modules:
CommonJS:
Именно поэтому Tree Shaking в Webpack по своей природе ориентирован на ESM и рассматривает CommonJS как ограниченный или частично непрозрачный источник зависимостей.
При использовании CommonJS в крупных проектах наблюдаются следующие эффекты:
Эти факторы особенно заметны в проектах с большим количеством сторонних npm-пакетов, не переведённых на ESM.