Минификация и tree shaking: взаимодействие

Базовый контекст оптимизаций в esbuild

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

Обе оптимизации работают на разных уровнях абстракции:

  • Tree shaking оперирует на уровне модулей и графа зависимостей
  • Минификация работает на уровне конкретного синтаксического дерева уже отобранного кода

Их взаимодействие напрямую влияет на итоговый размер и качество выходного JavaScript.


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

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

Tree shaking в esbuild основан на нескольких принципах:

  • анализ import / export в ES Modules
  • отслеживание достижимости символов (reachability analysis)
  • удаление кода, который не влияет на конечный результат выполнения

Пример:

// math.js
export function used() {
  return 1;
}

export function unused() {
  return 999;
}

// main.js
import { used } from './math.js';
console.log(used());

После tree shaking функция unused полностью исчезает из результирующего бандла.


Условия эффективности tree shaking

Tree shaking в esbuild зависит от нескольких факторов:

1. Формат модулей

  • ES Modules (import/export) — полностью поддерживаются
  • CommonJS (require/module.exports) — ограниченная или отсутствующая оптимизация

2. Побочные эффекты (side effects)

Если модуль или пакет может выполнять код при импорте, esbuild вынужден сохранять его:

// side-effect.js
console.log("module loaded");

export const value = 42;

Такой модуль не может быть полностью удалён, даже если экспорт не используется.

Контроль через package.json:

{
  "sideEffects": false
}

или точечное указание:

{
  "sideEffects": ["./init.js"]
}

Роль минификации после tree shaking

Минификация в esbuild включает несколько независимых преобразований:

  • удаление пробелов и переносов строк
  • сокращение имён переменных и функций
  • упрощение синтаксических конструкций
  • сворачивание выражений
  • оптимизация boolean- и control-flow конструкций

Флаг:

esbuild app.js --bundle --minify

эквивалентен включению трёх процессов:

  • minifyWhitespace
  • minifyIdentifiers
  • minifySyntax

Порядок взаимодействия tree shaking и минификации

Ключевая особенность esbuild заключается в том, что tree shaking выполняется до минификации.

Это создаёт важную цепочку оптимизации:

  1. Построение графа модулей
  2. Удаление неиспользуемых экспортов и импортов
  3. Формирование промежуточного AST без “мёртвого кода”
  4. Минификация оставшегося кода

Такой порядок критичен по причинам:

  • минификация не тратит ресурсы на код, который будет удалён
  • сокращается количество узлов AST, что ускоряет последующие этапы
  • уменьшается вероятность конфликтов при переименовании идентификаторов

Влияние tree shaking на эффективность минификации

Удаление неиспользуемого кода усиливает эффект минификации по нескольким направлениям:

1. Уменьшение пространства имён

После tree shaking остаётся меньше символов, что повышает эффективность:

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

2. Упрощение control-flow графа

Мёртвые ветки часто содержат:

  • условные блоки
  • исключения
  • вспомогательные функции

Их удаление позволяет минификатору выполнять более глубокую оптимизацию логики.

3. Улучшение свёртки выражений

Пример:

if (false) {
  doSomething();
}

После tree shaking блок может исчезнуть полностью, а минификация уже работает с упрощённой структурой.


Обратное влияние минификации на tree shaking

Хотя логически tree shaking предшествует минификации, некоторые трансформации влияют на дальнейшие стадии:

1. Инлайнинг и упрощение AST

Минификатор может:

  • сворачивать константы
  • упрощать выражения
  • устранять промежуточные переменные

Это помогает финальному этапу генерации кода, но не расширяет возможности tree shaking, так как тот уже завершён.

2. Переименование идентификаторов

Esbuild выполняет переименование после анализа зависимости, поэтому:

  • tree shaking работает с исходными символами
  • минификация не ломает ссылки между модулями

Ограничения взаимодействия

Tree shaking и минификация в esbuild имеют общие ограничения, связанные с динамическим кодом:

1. Динамические импорты

const mod = await import('./module.js');

Такие конструкции затрудняют статический анализ и могут ограничивать удаление кода.

2. Косвенные вызовы

const fn = obj["method"];
fn();

Сложность определения использования может привести к сохранению лишних частей.

3. CommonJS-совместимость

При использовании require():

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

Side effects как центральный фактор взаимодействия

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

Если пакет помечен как side-effect free:

{
  "sideEffects": false
}

esbuild может:

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

Если же side effects присутствуют:

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

Влияние code splitting на взаимодействие оптимизаций

При включённом code splitting:

esbuild app.js --bundle --splitting --format=esm

tree shaking выполняется отдельно для каждого чанка:

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

Это может приводить к разной степени сжатия между файлами в зависимости от локальной структуры зависимостей.


Keep names и влияние на цепочку оптимизаций

Флаг:

--keep-names

изменяет поведение минификации, но не tree shaking:

  • tree shaking остаётся неизменным
  • минификация перестаёт агрессивно переименовывать функции и классы

Это влияет на:

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

Итоговая модель взаимодействия

Внутренне взаимодействие tree shaking и минификации можно представить как последовательность преобразований:

  • построение графа модулей
  • анализ достижимости экспортов
  • удаление неиспользуемого кода
  • упрощение AST
  • синтаксическая минификация
  • переименование идентификаторов
  • генерация финального кода

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