Tree-shaking: условия и ограничения

Tree-shaking в Vite опирается не на собственную реализацию, а на цепочку инструментов, где ключевую роль играет сборщик Rollup в production-режиме и ES-модули как исходная форма кода. Именно поэтому эффективность удаления неиспользуемого кода зависит не от «включённой опции», а от структуры исходных модулей, характера импортов и поведения зависимостей.

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

Базовое требование для tree-shaking — наличие статически анализируемых импортов и экспортов. Это означает использование синтаксиса ES Modules:

export function a() {}
export function b() {}

и

import { a } from './module.js';

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

Если вместо этого используются CommonJS-модули:

module.exports = {
  a() {},
  b() {}
};

или

const mod = require('./module');

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

Влияние side effects

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

// module.js
console.log('module loaded');

export function a() {}

Даже если функция a не используется, сам факт наличия console.log делает модуль потенциально небезопасным для удаления.

Для управления этим поведением используется поле sideEffects в package.json:

{
  "sideEffects": false
}

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

Более точечная настройка:

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

В этом случае CSS и полифилы сохраняются, даже если их импорт не используется напрямую.

Статичность структуры кода

Tree-shaking требует, чтобы зависимости были определены статически. Любая динамическая конструкция ухудшает анализ графа модулей.

Проблемные паттерны:

const name = 'a';
export const mod = require('./' + name);
export const fn = condition ? a : b;

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

Наиболее критичным является динамический импорт через переменные:

import(`./modules/${name}.js`);

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

Влияние внешних зависимостей

Большая часть библиотек npm поставляется в формате CommonJS, что исторически снижает эффективность tree-shaking. Vite предпринимает шаги для преобразования таких модулей через esbuild на этапе dev-сервера и Rollup на этапе сборки, но результат зависит от качества самой библиотеки.

Хорошо оптимизированные библиотеки:

  • используют ES Modules в исходниках
  • публикуют отдельные ESM-сборки
  • корректно описывают sideEffects

Плохо оптимизированные библиотеки:

  • экспортируют один большой объект
  • используют module.exports
  • выполняют код при импорте

В таких случаях tree-shaking либо не работает, либо работает частично.

Барьеры анализа: ре-экспорты и barrel-файлы

Часто используется паттерн «barrel»:

export * from './a';
export * from './b';

или

export { a } from './a';
export { b } from './b';

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

Особенно проблемны конструкции вида:

export * as utils from './utils';

В этом случае сборщик может сохранить больший объём кода, так как точечная аналитика использования усложняется.

Минификация и финальный этап удаления кода

Tree-shaking в Vite фактически завершается на этапе Rollup, но финальная оптимизация часто усиливается минификатором (обычно esbuild или terser в зависимости от конфигурации).

Минификация дополнительно:

  • удаляет мёртвые ветки if (false)
  • убирает неиспользуемые локальные переменные
  • упрощает выражения

Однако минификатор не заменяет tree-shaking, а лишь дополняет его. Основное сокращение объёма кода происходит именно на уровне графа модулей.

Условная недостижимость удаления кода

Существуют сценарии, при которых tree-shaking практически перестаёт работать, даже если используется ES Modules:

  • наличие глобальных побочных эффектов
  • импорт модулей ради инициализации
  • использование паттернов «side-effect only import»
import './polyfills.js';

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

Частичное использование экспортов

Tree-shaking наиболее эффективен, когда экспортируются отдельные функции:

export function a() {}
export function b() {}

и импортируются точечно:

import { a } from './module';

Менее эффективно:

export default {
  a() {},
  b() {}
};

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

Влияние TypeScript и транспиляции

При использовании TypeScript исходный код преобразуется в JavaScript до этапа сборки. Если компилятор TypeScript настроен на CommonJS (module: "commonjs"), tree-shaking практически теряется.

Оптимальная конфигурация:

{
  "compilerOptions": {
    "module": "ESNext"
  }
}

Это позволяет сохранить ESM-структуру и не разрушить граф зависимостей.

Границы анализа Vite

Vite в режиме разработки не выполняет полноценный tree-shaking, так как использует нативные ES Modules в браузере. Оптимизация происходит только на этапе production build через Rollup.

Это означает, что:

  • в dev-режиме код загружается полностью
  • в production-режиме выполняется анализ и удаление неиспользуемого

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

Косвенные ограничения экосистемы

Tree-shaking ограничивается не только техническими факторами, но и архитектурными решениями библиотек:

  • единые «монолитные» экспорты
  • отсутствие разделения на модули
  • использование runtime-рефлексии
  • генерация кода на лету

Подобные подходы делают статический анализ либо неточным, либо невозможным.

Даже при идеальной конфигурации Vite результат tree-shaking всегда является функцией качества зависимостей и структуры кода, а не только возможностей сборщика.