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 в таких случаях не гарантируется.
Одним из ключевых факторов, ограничивающих удаление кода, являются побочные эффекты модулей. Побочный эффект — это любое выполнение кода при импорте модуля, которое не связано напрямую с экспортируемыми значениями.
// 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 на этапе сборки, но результат зависит от качества самой библиотеки.
Хорошо оптимизированные библиотеки:
sideEffectsПлохо оптимизированные библиотеки:
module.exportsВ таких случаях tree-shaking либо не работает, либо работает частично.
Часто используется паттерн «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:
import './polyfills.js';
В таких случаях модуль всегда включается в бандл, поскольку его выполнение считается частью программы.
Tree-shaking наиболее эффективен, когда экспортируются отдельные функции:
export function a() {}
export function b() {}
и импортируются точечно:
import { a } from './module';
Менее эффективно:
export default {
a() {},
b() {}
};
В этом случае сборщику приходится учитывать весь объект как единое значение, что ограничивает возможность удаления неиспользуемых частей.
При использовании TypeScript исходный код преобразуется в JavaScript
до этапа сборки. Если компилятор TypeScript настроен на CommonJS
(module: "commonjs"), tree-shaking практически
теряется.
Оптимальная конфигурация:
{
"compilerOptions": {
"module": "ESNext"
}
}
Это позволяет сохранить ESM-структуру и не разрушить граф зависимостей.
Vite в режиме разработки не выполняет полноценный tree-shaking, так как использует нативные ES Modules в браузере. Оптимизация происходит только на этапе production build через Rollup.
Это означает, что:
Такое разделение делает разработку быстрой, но переносит сложность оптимизации на финальную сборку.
Tree-shaking ограничивается не только техническими факторами, но и архитектурными решениями библиотек:
Подобные подходы делают статический анализ либо неточным, либо невозможным.
Даже при идеальной конфигурации Vite результат tree-shaking всегда является функцией качества зависимостей и структуры кода, а не только возможностей сборщика.