Как работает tree shaking в esbuild

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

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


Статический анализ модулей

Esbuild работает исключительно со статически анализируемыми структурами ES Modules:

  • import и export должны быть верхнеуровневыми
  • динамические вычисления не участвуют в базовой модели tree shaking
  • CommonJS преобразуется в промежуточную форму перед анализом

Пример исходного кода:

// util.js
export function used() {
  return "used";
}

export function unused() {
  return "unused";
}

// main.js
import { used } from "./util.js";

console.log(used());

После анализа esbuild формирует граф:

  • main.js → использует used
  • unused не имеет входящих ссылок

В результате unused полностью исключается из выходного бандла.


Построение графа зависимостей

Во внутренней модели esbuild каждый модуль превращается в набор:

  • экспортируемых символов
  • импортируемых символов
  • ссылок между ними

Принцип работы графа

  1. Начальная точка — entry file

  2. Все импортируемые модули добавляются в очередь обработки

  3. Для каждого модуля фиксируются:

    • какие символы реально используются
    • какие экспортируются, но не потребляются
  4. Итеративно вычисляется достижимое множество

Ключевая особенность: esbuild не просто удаляет файлы, а удаляет конкретные символы внутри файлов.


Связь tree shaking и ESM

Tree shaking в esbuild возможен только благодаря структуре ES Modules.

Почему ESM критичен

ESM имеет:

  • статическую структуру импортов
  • отсутствие динамического require на верхнем уровне (в идеале)
  • предсказуемую систему экспортов

Пример:

import { a } from "./mod.js";

Здесь a известен на этапе парсинга, что позволяет:

  • точно определить зависимость
  • удалить остальной код модуля, если он не используется

Оптимизация на уровне символов

Esbuild оперирует не файлами, а символами.

Типы символов:

  • функции
  • переменные
  • классы
  • re-export конструкции

Пример:

export const a = 1;
export const b = 2;
export const c = 3;

Если используется только a, то:

  • b и c удаляются
  • код не просто скрывается, а полностью исключается из AST

Inline-оптимизация и влияние на tree shaking

Tree shaking тесно связан с другими оптимизациями:

1. Inline constant folding

export const DEBUG = false;

Если DEBUG не используется, он исчезает полностью.

Если используется в условиях:

if (DEBUG) {
  doSomething();
}

esbuild может удалить целый блок:

// после оптимизации
// блок отсутствует

2. Dead code elimination

Если после удаления экспортов остаются неиспользуемые ветки:

function internal() {
  return 42;
}

export function api() {
  return "ok";
}

internal();

internal() может быть удалена, если:

  • нет внешних ссылок
  • нет side effects

Побочные эффекты и их влияние

Критический аспект tree shaking — анализ побочных эффектов (side effects).

Что считается побочным эффектом:

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

Пример:

console.log("module loaded");

export const x = 1;

Даже если x не используется, модуль может быть сохранён из-за console.log.


Флаг sideEffects в пакетах

В контексте npm-пакетов используется поле:

{
  "sideEffects": false
}

Оно позволяет esbuild:

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

Если sideEffects: true, поведение становится консервативным.


Работа с CommonJS

Esbuild поддерживает CommonJS, но tree shaking для него ограничен.

Проблема CommonJS

const a = require("./mod").a;

Здесь:

  • нельзя статически определить экспорт полностью
  • возможны динамические присваивания

Преобразование

Esbuild преобразует CommonJS → ESM-подобную модель:

  • require анализируется как импорт
  • но точность tree shaking ниже

Реэкспорты и их оптимизация

Простой re-export

export { a } from "./mod.js";

Esbuild может:

  • напрямую связать a с исходным модулем
  • удалить промежуточный слой

Namespace export

export * from "./mod.js";

Поведение зависит от использования:

  • если используется весь namespace — модуль сохраняется
  • если частично — удаляются ненужные символы

Влияние minification на tree shaking

Tree shaking в esbuild тесно интегрирован с минификацией.

Процесс объединён:

  1. анализ модулей
  2. удаление неиспользуемого кода
  3. переименование символов
  4. упаковка результата

Это позволяет:

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

Ограничения tree shaking

Несмотря на высокую эффективность, есть ограничения:

1. Динамический код

const name = "a";
export const obj = { [name]: 1 };

Такие конструкции усложняют анализ.


2. Побочные эффекты верхнего уровня

import "./polyfill.js";

Даже без экспорта файл сохраняется.


3. Косвенные зависимости

export * from dynamicPath;

Если путь вычисляется динамически — анализ невозможен.


Разница с другими сборщиками

Webpack

  • многоступенчатый анализ
  • reliance on usedExports
  • более медленный graph traversal

Rollup

  • один из самых точных tree shaker’ов
  • строгая ESM-модель

Esbuild

  • скорость приоритетнее полной семантической точности
  • агрессивная эвристика
  • минимальные накладные расходы

Внутренняя стратегия оптимизации

Esbuild использует несколько проходов:

  1. parsing (AST)
  2. linking (symbol graph)
  3. marking reachable exports
  4. pruning dead branches
  5. final code generation

Каждый этап оптимизирован под потоковую обработку, чтобы минимизировать память и время.


Поведение при ошибках анализа

Если esbuild не может точно определить использование:

  • символ считается потенциально используемым
  • код сохраняется
  • приоритет отдается корректности сборки

Это важный принцип: отсутствие ложных удалений важнее агрессивной оптимизации.


Итоговая модель работы tree shaking

Логика можно описать как:

  • построение графа символов
  • вычисление достижимости от entry point
  • удаление недостижимых узлов
  • учёт побочных эффектов
  • генерация оптимизированного кода

Эта модель позволяет esbuild достигать баланса между скоростью и качеством оптимизации, сохраняя предсказуемое поведение при сборке сложных JavaScript-приложений.