Tree Shaking: Inner Graph

Tree shaking в Webpack основан на статическом анализе ES-модулей и последующем построении графа зависимостей, в котором определяется, какие экспорты реально используются, а какие могут быть удалены как «мертвый код». Ключевую роль в этом процессе играет внутренняя модель связей между модулями и их содержимым, которую Webpack формирует в несколько этапов: module graph, dependency graph и inner graph анализа.

На этапе сборки Webpack преобразует каждый файл в модуль и строит глобальный граф зависимостей. Узлы графа — это модули, ребра — зависимости между ними, определяемые через import и export.

В ES-модулях зависимости статичны, что позволяет Webpack выполнять анализ без выполнения кода. Например:

import { a, b } from './utils';

console.log(a);

В таком случае Webpack фиксирует, что из ./utils используется только экспорт a, а b остаётся потенциальным кандидатом на удаление.

Однако простой module graph недостаточен для глубокого tree shaking, так как он оперирует только связями «модуль → модуль», но не раскрывает внутреннюю структуру экспортов и их использование внутри кода.

Inner Graph: анализ внутренней структуры модулей

Inner graph в Webpack — это расширение module graph, которое описывает зависимости не только между модулями, но и внутри самих модулей, на уровне экспортируемых сущностей.

Каждый модуль в Webpack представляется не как единый блок кода, а как набор экспортов, связанных с конкретными определениями внутри файла. Inner graph фиксирует:

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

Таким образом, inner graph — это граф более высокой детализации, где узлы уже не только модули, но и их экспортируемые символы.

Пример:

// utils.js
export function a() {
  return 'A';
}

export function b() {
  return 'B';
}

export function helper() {
  return a() + b();
}
// index.js
import { a } from './utils';

console.log(a());

Module graph фиксирует зависимость index.js → utils.js. Inner graph уточняет: используется только a, при этом b и helper остаются неиспользуемыми, но helper косвенно зависит от a и b.

Построение внутреннего графа экспортов

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

  1. Разбор AST каждого модуля (через acorn / parser)
  2. Определение экспортов и импортов
  3. Связывание импортов с конкретными экспортами (export binding)
  4. Построение ExportInfo для каждого символа
  5. Формирование inner dependency graph между ExportInfo

На этом уровне каждый экспорт становится объектом с состоянием использования:

  • used — экспорт используется напрямую
  • unused — экспорт не используется
  • side-effect dependent — экспорт участвует в цепочке побочных эффектов
  • re-export chain — экспорт переэкспортируется через другие модули

Связь inner graph и live bindings

ES-модули используют live bindings — импортируемые значения связаны с оригинальными переменными. Это означает, что Webpack не может просто удалить экспорт, не проверив его влияние на другие части графа.

Inner graph учитывает это через цепочки ссылок:

  • экспорт → локальное определение
  • локальное определение → выражения внутри модуля
  • выражения → другие экспорты или side effects

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

Side effects и влияние на inner graph

Флаг sideEffects в package.json влияет на формирование inner graph. Если модуль помечен как содержащий побочные эффекты:

{
  "sideEffects": false
}

Webpack может безопаснее удалять неиспользуемые экспорты, так как предполагается отсутствие глобальных изменений при импорте.

Если sideEffects: true, inner graph становится более консервативным:

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

Связь with usedExports и оптимизациями

Опция optimization.usedExports активирует маркировку используемых экспортов. На этом этапе Webpack не удаляет код, а только помечает его:

optimization: {
  usedExports: true
}

Inner graph используется именно для того, чтобы определить:

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

После этого этапа вступает Terser или другой минификатор, который выполняет фактическое удаление.

Module concatenation и влияние на inner graph

Scope hoisting (module concatenation) влияет на структуру inner graph, так как несколько модулей объединяются в единый скоуп.

При этом:

  • исчезают границы между модулями
  • inner graph становится внутрискоупным графом функций и переменных
  • экспортные узлы превращаются в локальные binding-и

Пример объединения:

До concatenation:

  • moduleA → export a
  • moduleB → import a

После:

  • единый scope
  • прямые ссылки на переменные без промежуточного module boundary

Inner graph в этом случае работает уже на уровне объединённого AST.

Dead Code Elimination и роль inner graph

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

  1. Маркировка usedExports (на основе inner graph)
  2. Упрощение графа зависимостей
  3. Минификация (Terser)
  4. Удаление unreachable code

Inner graph позволяет точно определить, какие узлы не имеют входящих рёбер использования. Такие узлы считаются кандидатами на удаление.

Пример:

export function used() {
  return 1;
}

export function unused() {
  return 2;
}

Если используется только used, inner graph помечает unused как изолированный узел.

Re-export цепочки и сложные зависимости

Особую сложность представляют re-export конструкции:

export { a } from './moduleA';
export { a as b } from './moduleB';

Inner graph в этом случае строит цепочку:

  • index.js → re-export
  • re-export → moduleA/moduleB export
  • moduleA export → local definition

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

Dynamic import и разрыв графа

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

import('./module').then(m => m.a());

разрывают статическую структуру inner graph. В этом случае:

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

Inner graph становится условным: часть узлов может быть недостижима в одном сценарии, но достижима в другом.

Итоговая роль inner graph в tree shaking

Inner graph в Webpack выступает как слой детализации между модульным графом и финальной оптимизацией. Он обеспечивает:

  • точное связывание экспортов и их использования
  • анализ цепочек re-export
  • учет side effects на уровне модулей и выражений
  • подготовку данных для dead code elimination
  • поддержку scope hoisting и объединения модулей

Именно благодаря inner graph tree shaking в Webpack становится не поверхностным удалением неиспользуемых модулей, а глубокой семантической оптимизацией кода на уровне отдельных символов и выражений.