Tree-shaking — это процесс статического анализа кода, при котором из конечного бандла удаляется неиспользуемый экспортируемый код. В контексте Rollup он является одной из ключевых оптимизаций, позволяющих формировать компактные и эффективные сборки. Однако эффективность tree-shaking напрямую зависит от того, в каком формате модулей написан исходный код. Наиболее важный фактор здесь — использование ES-модулей (ESM), поскольку именно их структура делает статический анализ возможным.
ES-модули (import/export) обладают строго определённой и предсказуемой структурой, которая анализируется без выполнения кода. Это принципиально отличает их от систем модулей CommonJS или AMD.
Ключевая особенность ES-модулей заключается в том, что их зависимости и экспорты известны на этапе парсинга:
import всегда находится на верхнем уровне модуляexport объявляется явно и не может быть изменён
динамическиБлагодаря этому Rollup может построить граф зависимостей, не исполняя код, а значит — точно определить, какие части используются, а какие нет.
CommonJS (require, module.exports) был
разработан как динамическая система модулей, ориентированная на
выполнение в рантайме. Это создаёт фундаментальные ограничения для
статического анализа.
Основные проблемы:
requireconst moduleName = getName();
const lib = require(moduleName);
Имя зависимости может быть вычислено во время выполнения, что делает невозможным статический анализ импортов.
module.exports.value = 1;
if (condition) {
module.exports.value = 2;
}
Экспорт может изменяться в зависимости от условий выполнения, что ломает предсказуемость структуры.
CommonJS экспортирует объект, который можно мутировать в любой момент:
module.exports = {};
module.exports.a = 1;
delete module.exports.a;
Такой подход делает невозможным точное определение используемых свойств.
Rollup строит граф зависимостей на основе следующих принципов:
Rollup использует AST (Abstract Syntax Tree), анализируя структуру файла:
import/exportЭто позволяет точно определить, какие узлы графа связаны.
Каждый модуль рассматривается как узел графа, а импорты — как рёбра:
A → B → C
→ D
Если из модуля A используется только часть API модуля B, Rollup может проследить цепочку до конкретного экспортируемого символа.
Если экспорт не используется ни в одном из downstream-модулей, он считается “dead code” и удаляется.
Пример:
// utils.js
export function used() {}
export function unused() {}
import { used } from './utils.js';
В итоговом бандле функция unused будет исключена.
Tree-shaking работает корректно только при отсутствии побочных эффектов. Если модуль выполняет код при импорте, Rollup вынужден сохранять его полностью.
// side-effect.js
console.log('module loaded');
export const value = 1;
Даже если value не используется,
console.log создаёт побочный эффект, и модуль не может быть
безопасно удалён.
ESM позволяет точнее контролировать такие ситуации через поле
sideEffects в package.json:
{
"sideEffects": false
}
Это сигнализирует сборщику, что модуль не имеет побочных эффектов и может быть безопасно сокращён.
ES-модули используют концепцию live bindings, которая играет важную роль в tree-shaking.
// counter.js
export let count = 0;
export function inc() {
count++;
}
import { count, inc } from './counter.js';
inc();
console.log(count);
Значение count не копируется, а остаётся связанным с
оригинальным модулем. Это означает:
В CommonJS такой модели нет — экспортируемые значения копируются, что делает оптимизацию небезопасной.
Несмотря на статическую природу ES-модулей, некоторые конструкции усложняют анализ:
import('./module.js').then(m => m.run());
Такой код создаёт отдельную точку разделения чанков и требует анализа на уровне графа чанков, а не отдельных экспортов.
export { a } from './a.js';
Rollup должен разворачивать такие цепочки до конечного определения, чтобы определить, используется ли символ.
import * as utils from './utils.js';
В этом случае сложно определить, какие именно части объекта используются:
utils.a();
Tree-shaking становится менее точным, если доступ к модулю происходит через namespace без явного указания символов.
Ключевая причина заключается в том, что tree-shaking основан на статическом анализе графа зависимостей, а не на выполнении кода.
ES-модули обеспечивают:
CommonJS, напротив, представляет собой runtime-систему, где:
Это делает невозможным построение точного графа использования кода.
Использование Babel или TypeScript может негативно влиять на tree-shaking, если они преобразуют ESM в CommonJS:
// исходный код
export function a() {}
После транспиляции:
module.exports.a = function () {};
Rollup теряет возможность точного анализа и вынужден включать больше кода, чем необходимо.
Поэтому важным условием эффективного tree-shaking является сохранение ESM-формата вплоть до этапа сборки.
Для корректной работы tree-shaking важно, чтобы пакет явно указывал ESM-версию:
{
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js"
}
Поле module позволяет сборщикам, включая Rollup,
использовать версию с ES-модулями, что обеспечивает максимальную
оптимизацию.
Tree-shaking в Rollup возможен только при соблюдении трёх условий:
ES-модули удовлетворяют этим требованиям полностью, тогда как CommonJS — нет, что делает ESM фундаментальной основой всей системы tree-shaking в Rollup.