Tree shaking основан на идее удаления кода, который не используется в конечной сборке. В современных бандлерах эта техника опирается на статическую структуру модулей, прежде всего на ESM (ECMAScript Modules), где зависимости и экспортируемые сущности известны до выполнения кода.
В контексте esbuild tree shaking реализуется как часть этапа
бандлинга и опирается на граф модулей, построенный из
import/export. Важное свойство ESM —
возможность анализировать зависимости без исполнения кода, что делает
возможным удаление неиспользуемых экспортов.
Для корректного удаления кода необходимы три условия:
Если хотя бы одно из условий нарушается, безопасность удаления кода становится недоказуемой, и бандлер вынужден сохранять больший объём кода.
CommonJS (require, module.exports) был
создан как динамическая система модулей. Он допускает выполнение
произвольного кода во время импорта, что делает статический анализ
значительно сложнее.
В CommonJS допустимы конструкции:
const mod = require(condition ? "./a" : "./b");
или:
const name = "utils";
const mod = require("./" + name);
Такие выражения невозможно корректно разрешить на этапе сборки без выполнения программы.
Следствие: бандлер не может построить точный граф зависимостей.
В CommonJS экспорт может изменяться в процессе выполнения:
module.exports = function () {};
module.exports = { a: 1 };
или даже:
exports.a = 1;
delete exports.a;
exports.a = 2;
Такая изменяемость нарушает предсказуемость структуры экспорта. Tree shaking требует неизменяемой экспортной модели.
CommonJS модули часто выполняют код сразу при загрузке:
console.log("module loaded");
globalState.init();
Даже если импортируется только часть API, сам факт подключения модуля запускает выполнение. Это делает невозможным безопасное удаление “неиспользуемых частей” — модуль считается единым побочным эффектом.
ESM предоставляет:
import/export;Пример:
export function a() {}
export function b() {}
Если используется только a, то b может быть
безопасно удалена при корректной настройке.
Внутренняя модель бандлера строится вокруг графа модулей и анализа доступности символов.
Ключевые этапы:
Tree shaking в большинстве конфигураций включён по умолчанию и не требует отдельного флага, но его эффективность зависит от формата модулей.
CommonJS не предоставляет явной структуры экспортов. В результате:
Пример:
if (process.env.NODE_ENV === "production") {
exports.debug = () => {};
}
Статический анализ не может гарантировать выполнение ветки, поэтому возможна утрата точности.
require("./setup")();
или:
const plugin = require(getPluginName());
Такие конструкции разрушают предсказуемость графа модулей.
Файлы вида:
module.exports = {
...require("./a"),
...require("./b")
};
создают дополнительный слой непрозрачной агрегации. Tree shaking становится невозможным на уровне отдельных функций, так как всё объединяется в один объект.
Важный механизм, влияющий на удаление кода — поле:
{
"sideEffects": false
}
Оно сообщает бандлеру, что модули пакета не имеют побочных эффектов при импорте.
При true или отсутствии поля:
В CommonJS это особенно критично, так как побочные эффекты встречаются чаще и труднее анализируются.
При смешанном использовании модулей возникает слой совместимости:
Пример проблемы:
import * as lib from "cjs-lib";
Если cjs-lib — CommonJS, то lib становится
обёрткой над module.exports, а не набором статически
анализируемых экспортов.
Это ограничивает возможность удаления неиспользуемых частей.
import * as utils from "./utils";
В ESM это всё ещё анализируемо, но в CommonJS-совместимых модулях:
utils часто сохраняется полностью;exports.a = require("./a");
exports.b = require("./b");
или:
Object.assign(exports, require("./a"));
Такие конструкции превращают модуль в “чёрный ящик”, где невозможно определить, какие части действительно используются.
esbuild часто выполняет внутреннюю нормализацию:
В результате:
Tree shaking тесно связан с DCE (Dead Code Elimination):
Однако CommonJS снижает эффективность DCE, поскольку:
Если пакет публикуется только в CommonJS:
Модули с инициализацией:
Такие модули никогда не считаются безопасными для удаления.
Большие index.js:
export * from "./a";
export * from "./b";
export * from "./c";
В ESM это работает лучше, но в CJS-обёртках часто превращается в единый блок без возможности точечного удаления.
Tree shaking эффективен только тогда, когда система модулей:
CommonJS нарушает все три свойства, поэтому любые инструменты, включая esbuild, вынуждены применять консервативные стратегии оптимизации.