Tree shaking в Webpack основан на статическом анализе ES-модулей и последующем построении графа зависимостей, в котором определяется, какие экспорты реально используются, а какие могут быть удалены как «мертвый код». Ключевую роль в этом процессе играет внутренняя модель связей между модулями и их содержимым, которую Webpack формирует в несколько этапов: module graph, dependency graph и inner graph анализа.
На этапе сборки Webpack преобразует каждый файл в модуль и строит
глобальный граф зависимостей. Узлы графа — это модули, ребра —
зависимости между ними, определяемые через import и
export.
В ES-модулях зависимости статичны, что позволяет Webpack выполнять анализ без выполнения кода. Например:
import { a, b } from './utils';
console.log(a);
В таком случае Webpack фиксирует, что из ./utils
используется только экспорт a, а b остаётся
потенциальным кандидатом на удаление.
Однако простой module graph недостаточен для глубокого tree shaking, так как он оперирует только связями «модуль → модуль», но не раскрывает внутреннюю структуру экспортов и их использование внутри кода.
Inner graph в Webpack — это расширение module graph, которое описывает зависимости не только между модулями, но и внутри самих модулей, на уровне экспортируемых сущностей.
Каждый модуль в Webpack представляется не как единый блок кода, а как набор экспортов, связанных с конкретными определениями внутри файла. Inner graph фиксирует:
Таким образом, inner graph — это граф более высокой детализации, где узлы уже не только модули, но и их экспортируемые символы.
Пример:
// utils.js
export function a() {
return 'A';
}
export function b() {
return 'B';
}
export function helper() {
return a() + b();
}
// index.js
import { a } from './utils';
console.log(a());
Module graph фиксирует зависимость index.js → utils.js.
Inner graph уточняет: используется только a, при этом
b и helper остаются неиспользуемыми, но
helper косвенно зависит от a и
b.
Webpack при анализе модуля выполняет несколько стадий:
На этом уровне каждый экспорт становится объектом с состоянием использования:
ES-модули используют live bindings — импортируемые значения связаны с оригинальными переменными. Это означает, что Webpack не может просто удалить экспорт, не проверив его влияние на другие части графа.
Inner graph учитывает это через цепочки ссылок:
Таким образом, даже если экспорт напрямую не используется, он может быть сохранён, если участвует в цепочке вычислений.
Флаг sideEffects в package.json влияет на
формирование inner graph. Если модуль помечен как содержащий побочные
эффекты:
{
"sideEffects": false
}
Webpack может безопаснее удалять неиспользуемые экспорты, так как предполагается отсутствие глобальных изменений при импорте.
Если sideEffects: true, inner graph становится более
консервативным:
Опция optimization.usedExports активирует маркировку
используемых экспортов. На этом этапе Webpack не удаляет код, а только
помечает его:
optimization: {
usedExports: true
}
Inner graph используется именно для того, чтобы определить:
После этого этапа вступает Terser или другой минификатор, который выполняет фактическое удаление.
Scope hoisting (module concatenation) влияет на структуру inner graph, так как несколько модулей объединяются в единый скоуп.
При этом:
Пример объединения:
До concatenation:
После:
Inner graph в этом случае работает уже на уровне объединённого AST.
Удаление неиспользуемого кода происходит не в одном месте, а в несколько этапов:
Inner graph позволяет точно определить, какие узлы не имеют входящих рёбер использования. Такие узлы считаются кандидатами на удаление.
Пример:
export function used() {
return 1;
}
export function unused() {
return 2;
}
Если используется только used, inner graph помечает
unused как изолированный узел.
Особую сложность представляют re-export конструкции:
export { a } from './moduleA';
export { a as b } from './moduleB';
Inner graph в этом случае строит цепочку:
Такие цепочки могут быть глубоко вложенными, и Webpack обязан проследить полный путь использования, чтобы не удалить нужный экспорт.
Динамические импорты:
import('./module').then(m => m.a());
разрывают статическую структуру inner graph. В этом случае:
Inner graph становится условным: часть узлов может быть недостижима в одном сценарии, но достижима в другом.
Inner graph в Webpack выступает как слой детализации между модульным графом и финальной оптимизацией. Он обеспечивает:
Именно благодаря inner graph tree shaking в Webpack становится не поверхностным удалением неиспользуемых модулей, а глубокой семантической оптимизацией кода на уровне отдельных символов и выражений.