При работе с esbuild механизм удаления неиспользуемого кода (tree shaking) основан на статическом анализе модулей ECMAScript. Поведение оптимизатора определяется не только фактом отсутствия импортов, но и множеством косвенных факторов: наличием побочных эффектов, формой экспорта, типом модульной системы, а также структурой зависимостей.
В основе логики лежит принцип: код удаляется только тогда, когда можно строго доказать, что он не влияет на выполнение программы. Любая неопределённость приводит к сохранению фрагмента.
Официальное описание поведения и ограничений сборщика отражено в документации esbuild documentation.
Самая частая причина, по которой код не удаляется, — наличие потенциальных побочных эффектов (side effects).
Фрагмент считается «опасным для удаления», если он может изменить состояние среды вне текущего модуля:
window,
globalThis)Даже если экспорт не используется, сам факт выполнения кода делает его значимым.
Пример:
// module.js
window.analytics = window.analytics || [];
window.analytics.push("init");
export const unused = 42;
Переменная unused может быть удалена, но инициализация
аналитики останется, поскольку она выполняется при загрузке модуля.
Система упаковки ориентируется на поле sideEffects в
package.json. Оно управляет тем, можно ли безопасно удалять
неиспользуемые файлы целиком.
{
"sideEffects": false
}
При значении false предполагается, что все модули
«чистые», и esbuild может агрессивно удалять неиспользуемые экспорты и
даже целые файлы.
Если поле отсутствует или установлено в true, сборщик
вынужден считать каждый файл потенциально значимым.
Особая форма:
{
"sideEffects": ["*.css", "*.scss"]
}
В этом случае удаление кода ограничивается, и стили всегда сохраняются.
Tree shaking в esbuild максимально эффективен при использовании ES Modules:
export const a = 1;
export const b = 2;
Если импортируется только a, b может быть
удалён.
Однако в CommonJS ситуация иная:
module.exports = {
a: 1,
b: 2
};
Здесь экспорт представляет собой динамический объект, и статически определить неиспользуемые части сложнее. В результате esbuild часто сохраняет больше кода, чем необходимо.
Особенно проблемны конструкции:
require() с переменными аргументамиmodule.exports после объявленияЛюбая динамика в структуре импорта блокирует удаление кода:
const mod = await import(someVariable);
Поскольку значение someVariable неизвестно на этапе
сборки, esbuild обязан сохранить весь потенциально доступный код.
Аналогично:
require(path.join(base, name));
Такие конструкции полностью отключают точечную оптимизацию зависимостей.
Реэкспорт увеличивает сложность анализа:
export { a } from "./moduleA";
export { b } from "./moduleB";
Если промежуточные модули содержат побочные эффекты или сложные зависимости, esbuild не всегда может безопасно удалить их части.
Особенно проблемны цепочки:
index.js → re-export.js → core.js → utils.js
Любая неоднозначность в середине цепочки приводит к сохранению лишнего кода.
Код, выполняемый при загрузке модуля, почти всегда сохраняется:
const start = Date.now();
console.log("module loaded");
Даже если переменная start не используется, сама
операция console.log является побочным эффектом.
Особенность esbuild заключается в том, что он не выполняет интерпретацию логики, а только анализирует структуру. Поэтому он не может доказать, что такой код безопасно удалить.
Для функций и вызовов без побочных эффектов используется специальная аннотация:
const x = /*#__PURE__*/ createConfig();
Она сообщает оптимизатору, что вызов может быть удалён, если результат не используется.
Без этой аннотации функция рассматривается как потенциально опасная, особенно если она импортирована из внешнего модуля.
Удаление может не происходить, если экспорт используется неявно:
export const config = {
value: compute()
};
Если compute() имеет побочные эффекты, весь объект
сохраняется, даже если config не импортируется.
Также косвенные ссылки:
globalThis.config = config;
Такие конструкции полностью исключают возможность tree shaking.
Оптимизация удаления кода зависит от включённых режимов:
bundle: объединяет модули, но не всегда агрессивно
удаляет кодminify: усиливает удаление, но ограничен сохранением
семантикиПример:
esbuild input.js --bundle --minify
Даже в этом режиме код с побочными эффектами сохраняется.
Если модуль объявлен как external:
external: ["react"]
esbuild не анализирует его содержимое. Всё, что зависит от него, может быть сохранено в исходном виде, без оптимизации.
Несмотря на высокую скорость, esbuild не выполняет глубокую интерпретацию кода. Это приводит к сохранению фрагментов в следующих случаях:
Любая неопределённость трактуется в пользу сохранения кода.
if (process.env.NODE_ENV !== "production") {
debugLog();
}
Даже если сборка выполняется в production-режиме, без явной
подстановки define, esbuild может сохранить ветку.
Оптимизация становится возможной только при явной фиксации:
--define:process.env.NODE_ENV='"production"'
Любое обращение к глобальным объектам усложняет удаление:
window.myLib = factory();
или
globalThis.state = init();
Такие конструкции связывают модуль с внешним состоянием, делая его потенциально значимым независимо от использования экспортов.
Причины сохранения кода в esbuild почти всегда сводятся к одной из категорий:
Поведение сборщика строится на принципе консервативной безопасности: любой сомнительный фрагмент сохраняется, чтобы не нарушить семантику приложения.