Сохранение экспортов и предотвращение tree shaking экспортов

В основе оптимизации esbuild лежит статический анализ ES Modules. При сборке модульного графа инструмент определяет, какие экспорты действительно используются в конечной точке входа, и удаляет всё, что не влияет на результат выполнения.

Tree shaking в esbuild опирается на несколько ключевых условий:

  • используется формат ESM (format: "esm") либо корректно транспилируемый CJS-совместимый код
  • отсутствуют побочные эффекты в выражениях, которые можно безопасно удалить
  • экспортированные сущности не имеют внешних ссылок в графе зависимостей

Если экспорт не используется ни одной частью графа, он считается «мертвым» и исключается из бандла.

Причины удаления экспортов в процессе оптимизации

Удаление экспортов происходит не только из-за отсутствия использования, но и из-за анализа их «значимости»:

  • экспортируется функция, которая нигде не вызывается
  • экспортируется константа, не участвующая в вычислениях
  • модуль импортируется, но его экспортные значения не читаются
  • цепочки re-export (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 будут удалены как неиспользуемые экспортируемые значения.

Сохранение экспортов через сохранение ссылок в графе

Главный способ «сохранить» экспорт — сделать его достижимым из точки входа графа.

Любое из условий сохраняет экспорт:

  • прямой импорт
  • косвенный импорт через re-export
  • использование в динамических выражениях, которые нельзя статически доказать как неиспользуемые
  • включение в возвращаемую структуру, используемую далее
export const logger = () => console.log("keep");

export const unused = () => console.log("remove");

Если logger импортируется хотя бы в одном месте, он сохраняется; unused будет удалён.

Поведение re-export и влияние на tree shaking

Конструкции повторного экспорта влияют на анализ графа:

// lib.js
export const a = 10;
export const b = 20;

// reexport.js
export { a, b } from "./lib.js";

Если в конечной сборке используется только a, esbuild:

  • сохраняет a
  • удаляет b
  • оптимизирует промежуточный re-export слой при возможности

Однако при сложных цепочках re-export с побочными эффектами оптимизация становится консервативнее.

Побочные эффекты как основной фактор сохранения кода

Tree shaking полностью зависит от способности определить отсутствие побочных эффектов.

Код считается имеющим побочные эффекты, если:

  • выполняет действия вне своей локальной области (изменяет глобальные переменные, DOM, состояние модулей)
  • вызывает функции, результат которых неизвестен на этапе анализа
  • содержит импорт модулей с неизвестным поведением

Пример:

export const value = doSomething();

Если doSomething() не может быть доказан как чистый вызов, экспорт value не будет удалён даже при отсутствии явного использования.

Управление tree shaking через package.json

Одним из ключевых механизмов влияния на сохранение или удаление экспортов является поле sideEffects.

Полное отключение предположений о чистоте модулей

{
  "sideEffects": true
}

Такой режим заставляет bundler считать все модули потенциально «грязными», что резко снижает агрессивность удаления кода. Экспорты сохраняются значительно чаще, даже если формально не используются.

Точечное управление побочными эффектами

{
  "sideEffects": [
    "./src/polyfill.js",
    "*.css"
  ]
}

В этом режиме tree shaking сохраняется для большинства модулей, но отдельные файлы всегда включаются в бандл.

Отключение tree shaking в esbuild

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();

Такие вызовы могут быть удалены, если результат не используется.

Чтобы предотвратить удаление:

  • нельзя помечать вызовы как pure
  • нельзя скрывать побочные эффекты внутри выражений, которые анализируются как чистые
const value = createObject();

В отличие от аннотированного варианта, такой код сохраняется при невозможности доказать его неиспользование.

Особенности сохранения экспортов в ESM и CJS режимах

В ESM:

  • tree shaking наиболее точен
  • анализ статический и предсказуемый
  • экспорт может быть удалён на уровне графа модулей

В CJS:

  • анализ ограничен
  • экспорты часто сохраняются из-за динамической природы module.exports
  • оптимизация более консервативна
// CJS
module.exports = {
  a: 1,
  b: 2,
};

Даже при частичном использовании esbuild может сохранить весь объект экспорта.

Роль структуры модулей в сохранении экспортов

Сильное влияние оказывает архитектура:

  • «баррельные» файлы (index.js с re-export) усиливают агрегацию и упрощают удаление
  • глубокие цепочки импорта увеличивают вероятность частичного сохранения
  • динамические импорты (import()) ослабляют анализ графа

Оптимальная структура для максимального tree shaking:

  • плоские модули
  • минимизация re-export без необходимости
  • отсутствие скрытых побочных эффектов в верхнем уровне модулей

Практическое влияние на сохранение API поверхности

Сохранение экспортов часто конфликтует с минимизацией бандла.

Сценарии, при которых экспорты гарантированно сохраняются:

  • экспорт используется через публичный API библиотеки
  • модуль участвует в динамическом импорте
  • экспорт связан с глобальными объектами
  • отключён tree shaking на уровне сборки

Сценарии удаления:

  • экспорт не используется ни в одном пути графа
  • код признан чистым и не влияет на результат
  • отсутствуют побочные эффекты и внешние ссылки