Отладка: что попало в бандл и почему

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

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


Построение графа модулей и первичная причина включения кода

Rollup строит граф зависимостей, начиная с входных точек. Каждый import фиксируется как ребро графа, а содержимое модуля анализируется на предмет:

  • экспортируемых значений
  • внутренних вызовов функций
  • побочных эффектов на верхнем уровне
  • динамических импортов

Если модуль включён в граф, он уже потенциально может попасть в бандл. Исключение возможно только при одновременном выполнении условий:

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

Побочные эффекты как главный фактор «лишнего» кода

На практике большинство «непонятно откуда взявшегося» кода сохраняется из-за побочных эффектов.

Побочный эффект в контексте Rollup — любое выполнение кода при загрузке модуля:

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

Rollup консервативен: если модуль потенциально имеет side effects, он сохраняется целиком.

Пример типичной проблемы

// utils.js
console.log('init utils');

export function sum(a, b) {
  return a + b;
}

Даже если sum не используется, модуль может остаться в бандле из-за console.log.


sideEffects в package.json и его влияние

Одним из ключевых источников ошибок tree-shaking является поле sideEffects в package.json.

Варианты поведения:

  • sideEffects: false — весь пакет считается чистым
  • массив файлов — указание конкретных модулей с эффектами
  • отсутствие поля — Rollup предполагает осторожную стратегию

Типичная ситуация деградации оптимизации

Библиотека заявляет:

{
  "sideEffects": false
}

Но фактически содержит:

import './polyfills.js';

Если polyfills изменяют глобальную среду, но пакет объявлен «чистым», результатом может стать:

  • поломка логики
  • или, наоборот, сохранение всего кода без tree-shaking из-за косвенных зависимостей

PURE-аннотации и их роль в удалении вызовов

Rollup поддерживает интерпретацию аннотаций вида:

/*#__PURE__*/

Они сигнализируют, что вызов функции не имеет побочных эффектов:

const instance = /*#__PURE__*/ createInstance();

Если результат не используется, вызов может быть удалён.

Причины, почему PURE не сработал:

  • результат функции используется косвенно
  • функция мутирует внешнее состояние
  • плагин трансформации удалил аннотацию
  • код приходит из CommonJS и не аннотирован

CommonJS как источник «неудаляемого» кода

При использовании @rollup/plugin-commonjs поведение tree-shaking резко меняется.

CommonJS не предоставляет статически анализируемых экспортов, поэтому:

  • Rollup вынужден эмулировать модуль
  • экспорт становится динамическим объектом
  • внутренние ветки кода сохраняются

Пример:

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

Если используется только a, Rollup не всегда может гарантированно удалить b.


Динамические импорты и расширение графа

import() создаёт отдельные чанки и влияет на анализ:

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

Причины попадания кода в бандл:

  • условные динамические импорты с неопределёнными путями
  • плагины, преобразующие динамический импорт в статический require
  • объединение чанков через manualChunks

Инструменты визуализации бандла

Основная задача отладки — не гадать, а видеть структуру итогового кода.

rollup-plugin-visualizer

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

  • строит treemap
  • показывает вклад модулей
  • отображает дублирование зависимостей

Причины полезности:

  • сразу видно «тяжёлые» модули
  • легко определить неожиданные зависимости
  • помогает выявить отсутствие tree-shaking

Source maps как инструмент обратного трассирования

Sourcemaps позволяют связать код бандла с исходными модулями:

output: {
  sourcemap: true
}

При отладке важно учитывать:

  • минифицированный код скрывает реальные границы модулей
  • inline-sourcemap упрощает локальный анализ
  • external sourcemap удобнее для CI

Логирование процесса сборки

Rollup позволяет получать информацию о том, почему модуль включён:

  • через onwarn
  • через плагины (buildStart, resolveId, load, transform)
  • через debug-вывод плагинов

Особенно полезен анализ transform, где можно увидеть:

  • итоговый AST модуля
  • удалённые/оставленные экспорты
  • модификации кода плагинами

Tree-shaking и его ограничения

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

  • невозможность анализа runtime-логики
  • слабая работа с динамическими объектами
  • зависимость от корректных аннотаций модулей

Пример, где tree-shaking бессилен:

const mod = condition ? require('./a') : require('./b');
mod.run();

Такой код приводит к сохранению обеих веток.


preserveModules и ложное ощущение «лишнего кода»

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

preserveModules: true

каждый модуль сохраняется как отдельный файл.

Это часто воспринимается как «в бандл попало лишнее», хотя фактически:

  • tree-shaking работает
  • но структура не сливается

external и ошибочное включение зависимостей

Если модуль не помечен как external, он может попасть в бандл целиком.

external: ['react']

При отсутствии настройки:

  • библиотека React может быть встроена
  • увеличивается размер бандла
  • появляется дублирование зависимостей

Почему «неиспользуемый» экспорт не удалился

Типовые причины:

  • экспорт используется косвенно через re-export
  • объект экспортируется целиком
  • используется spread:
export default {
  ...utils
};
  • наличие getter/setter с побочными эффектами

Стратегия анализа итогового бандла

Последовательная проверка причин включения кода обычно сводится к следующему:

  1. Проверка графа зависимостей (визуализатор)
  2. Проверка sideEffects (package.json и module)
  3. Анализ CommonJS переходников
  4. Проверка PURE-аннотаций
  5. Анализ динамических импортов
  6. Проверка external-конфигурации
  7. Сопоставление через sourcemap

Частые диагностические сигналы

Некоторые признаки прямо указывают на причину включения кода:

  • большой модуль без tree-shaking → side effects
  • дублирование функций → CJS или несколько entry points
  • неожиданные polyfills → импорт побочных модулей
  • весь пакет включён → sideEffects не настроен
  • невозможность удаления веток → динамический require

Поведение плагинов как скрытый источник включения кода

Плагины Rollup могут существенно менять итоговый результат:

  • заменять ES на CommonJS
  • инлайнить зависимости
  • добавлять обёртки
  • генерировать дополнительный runtime

Особенно критично:

  • babel-плагины
  • typescript transpile-only режим
  • legacy polyfill плагины

Итоговая логика попадания кода в бандл

Любой фрагмент кода в итоговом bundle объясняется комбинацией факторов:

  • достижимость через граф импортов
  • отсутствие гарантий отсутствия side effects
  • ограничения статического анализа
  • вмешательство плагинов трансформации
  • формат модулей (ESM vs CJS)
  • конфигурация output и treeshake

Разбор конкретного случая всегда сводится к обратной трассировке: от куска бандла к модулю, от модуля к причине сохранения в графе.