В основе оптимизации esbuild лежит статический анализ ES Modules. При сборке модульного графа инструмент определяет, какие экспорты действительно используются в конечной точке входа, и удаляет всё, что не влияет на результат выполнения.
Tree shaking в esbuild опирается на несколько ключевых условий:
format: "esm") либо корректно
транспилируемый CJS-совместимый кодЕсли экспорт не используется ни одной частью графа, он считается «мертвым» и исключается из бандла.
Удаление экспортов происходит не только из-за отсутствия использования, но и из-за анализа их «значимости»:
export { x } from ...) не приводят к
использованию x в конечной сборкеПример:
// math.js
export const a = 1;
export const b = 2;
export const c = 3;
// index.js
import { a } from "./math.js";
console.log(a);
В результате сборки b и c будут удалены как
неиспользуемые экспортируемые значения.
Главный способ «сохранить» экспорт — сделать его достижимым из точки входа графа.
Любое из условий сохраняет экспорт:
export const logger = () => console.log("keep");
export const unused = () => console.log("remove");
Если logger импортируется хотя бы в одном месте, он
сохраняется; unused будет удалён.
Конструкции повторного экспорта влияют на анализ графа:
// lib.js
export const a = 10;
export const b = 20;
// reexport.js
export { a, b } from "./lib.js";
Если в конечной сборке используется только a,
esbuild:
abОднако при сложных цепочках re-export с побочными эффектами оптимизация становится консервативнее.
Tree shaking полностью зависит от способности определить отсутствие побочных эффектов.
Код считается имеющим побочные эффекты, если:
Пример:
export const value = doSomething();
Если doSomething() не может быть доказан как чистый
вызов, экспорт value не будет удалён даже при отсутствии
явного использования.
Одним из ключевых механизмов влияния на сохранение или удаление
экспортов является поле sideEffects.
{
"sideEffects": true
}
Такой режим заставляет bundler считать все модули потенциально «грязными», что резко снижает агрессивность удаления кода. Экспорты сохраняются значительно чаще, даже если формально не используются.
{
"sideEffects": [
"./src/polyfill.js",
"*.css"
]
}
В этом режиме tree shaking сохраняется для большинства модулей, но отдельные файлы всегда включаются в бандл.
esbuild предоставляет прямую настройку поведения:
esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
treeShaking: false,
});
При отключении:
Этот режим используется при отладке или при работе с библиотеками, где важна полная сохранность API поверхности.
Экспорт сохраняется, если он становится частью внешнего контракта:
export function init() {}
globalThis.initApp = init;
Даже если init не импортируется напрямую, esbuild обязан
сохранить его, поскольку он используется через глобальное
присваивание.
Подобные конструкции полностью блокируют tree shaking для соответствующих символов.
esbuild распознаёт аннотации вида:
/* @__PURE__ */ createObject();
Такие вызовы могут быть удалены, если результат не используется.
Чтобы предотвратить удаление:
const value = createObject();
В отличие от аннотированного варианта, такой код сохраняется при невозможности доказать его неиспользование.
В ESM:
В CJS:
module.exports// CJS
module.exports = {
a: 1,
b: 2,
};
Даже при частичном использовании esbuild может сохранить весь объект экспорта.
Сильное влияние оказывает архитектура:
index.js с re-export) усиливают
агрегацию и упрощают удалениеimport()) ослабляют анализ
графаОптимальная структура для максимального tree shaking:
Сохранение экспортов часто конфликтует с минимизацией бандла.
Сценарии, при которых экспорты гарантированно сохраняются:
Сценарии удаления: