Tree shaking в React проектах: как реально уменьшить бандл

Разбираем tree shaking в React проектах: как он работает в Vite, Webpack и Next.js, где ломается и какие практики реально уменьшают размер бандла.

27 марта 2026 г.15 минLexicon Team

Введение

Запрос tree shaking в React проектах обычно появляется в одном из двух случаев. Либо фронтенд-команда видит, что стартовый бандл разросся до неприятных размеров, либо на собеседовании спрашивают, чем отличается формальная оптимизация от реально работающей. В обоих сценариях поверхностного ответа недостаточно. Сказать, что сборщик "сам удаляет неиспользуемый код", легко. Сложнее объяснить, почему после такого ответа в проекте все еще приезжает тяжелая библиотека целиком.

Если коротко, tree shaking, то есть удаление неиспользуемых ветвей модульного графа, работает не на уровне намерения разработчика, а на уровне доказуемости. Сборщик должен понять, что конкретный импорт безопасно выбросить, и что удаление модуля не сломает побочные эффекты. Поэтому в React-проекте итог зависит не только от Vite, Webpack или Next.js, но и от того, как вы строите экспорт, как оформляете barrel-файлы и что именно тянете из npm. Для общего контекста по размеру клиентского кода полезно отдельно посмотреть оптимизацию бандла в React, а разницу между уменьшением чанка и отложенной загрузкой удобно рассматривать вместе со статьей про code splitting и lazy loading.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Что такое tree shaking на практике

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

Простой пример выглядит так:

// math.ts
export function sum(a: number, b: number) {
  return a + b;
}

export function multiply(a: number, b: number) {
  return a * b;
}

export function slowDebugReport(values: number[]) {
  return values.map((value) => `${value}:${value * value}`).join(",");
}
// App.tsx
import { sum } from "./math";

export function App() {
  return <div>{sum(2, 3)}</div>;
}

Если math.ts не содержит побочных эффектов, а сборщик работает с ES modules, то multiply и slowDebugReport могут не попасть в production-бандл. Но это работает только пока модуль остается "чистым". Достаточно добавить в него что-то вроде console.log("math loaded"), и сборщик уже должен относиться к модулю осторожнее.

В React-проектах проблема редко связана с такими учебными функциями. Чаще она возникает в UI-китах, файлах с иконками, утилитах дат, слоях аналитики и индексных файлах, которые реэкспортируют все подряд. Именно поэтому tree shaking надо обсуждать вместе со структурой проекта, а не как галочку у bundler.

Архитектура: где именно tree shaking принимает решение

Полезно смотреть на tree shaking как на конвейер из четырех этапов.

  1. TypeScript или Babel превращает исходники в модульный граф.
  2. Bundler строит зависимости между entry points, локальными модулями и пакетами из node_modules.
  3. Анализатор определяет, какие exports реально используются.
  4. Минификатор и оптимизатор вырезают не достижимые ветки и схлопывают остаток.

В React-приложении этот процесс обычно проходит через несколько слоев:

  • entry point приложения;
  • роутинг или layout-слой;
  • feature-модули;
  • shared-утилиты и UI-компоненты;
  • внешние библиотеки.

Слабое место почти всегда находится на границе между слоями. Допустим, компонент страницы импортирует одну кнопку, но берет ее не из конкретного файла, а из components/index.ts, который заодно реэкспортирует модальные окна, таблицы, аналитические хелперы и тему. Дальше сборщик обязан разбираться, нет ли в цепочке побочных эффектов, циклических связей или CommonJS-перехода. Чем менее прозрачен граф, тем хуже работает вырезание мертвого кода.

Упрощенная схема выглядит так:

// components/index.ts
export * from "./Button";
export * from "./Modal";
export * from "./Charts";
export * from "./theme/registerTheme";

registerTheme может инициализировать тему при импорте. Формально страница попросила только Button, но фактически сборщик видит агрегирующий модуль с потенциальными побочными эффектами. В этот момент tree shaking часто перестает быть агрессивным.

Если вам нужно шире смотреть на границы ответственности между модулями, это хорошо сочетается с материалом про архитектуру больших React-приложений и статьей про проектирование масштабируемого React frontend.

Когда tree shaking работает хорошо, а когда почти бесполезен

Ниже не список догм, а таблица принятия решения.

СценарийЧто происходитПомогает ли tree shakingОграничение
Именованные exports из локального ESM-модуляСборщик видит точные связи между импортом и экспортомДа, почти всегдаНе должно быть побочных эффектов при импорте
Импорт из barrel-файла с export *Граф становится менее прозрачнымЧастичноРеэкспорты и сайд-эффекты могут удержать лишний код
CommonJS-зависимостьЭкспорты часто вычисляются динамическиСлабоBundler хуже доказывает, что код безопасно удалить
Библиотека с глобальной инициализациейИмпорт сам меняет окружениеПочти нетУдалять модуль опасно по смыслу
Code splitting через lazy()Чанк загружается позжеTree shaking помогает внутри чанкаНе уменьшает число чанков само по себе
Next.js клиентский компонент с тяжелой библиотекойКод попадает в клиентскую часть дереваИногдаНужна отдельная работа с границей server/client

Практический вывод такой: tree shaking лучше всего работает на простых, предсказуемых ESM-модулях с именованными экспортами. Чем больше магии, агрегации и глобальной инициализации, тем меньше эффект.

Пример, где tree shaking ломается незаметно

Одна из самых дорогих ошибок в React-проектах выглядит безобидно. Команда делает удобный barrel-файл и начинает импортировать все компоненты из одной точки. DX становится приятнее, но размер бандла ползет вверх.

// ui/index.ts
export { Button } from "./Button";
export { Input } from "./Input";
export { Dialog } from "./Dialog";
export { formatMoney } from "../lib/formatMoney";
export { initMonitoring } from "../lib/monitoring";

initMonitoring();
// CheckoutPage.tsx
import { Button } from "@/ui";

export function CheckoutPage() {
  return <Button>Оплатить</Button>;
}

На ревью такой код могут пропустить, потому что страница импортирует только Button. Но initMonitoring() исполняется при импорте @/ui, а значит модуль уже не чистый. В production это проявляется так:

  • bundle analyzer показывает, что вместе с кнопкой приехала аналитика;
  • критический путь загрузки вырос на десятки килобайт;
  • падает эффективность кэширования, потому что общий чанк меняется чаще;
  • команда думает, что "Vite плохо трясет дерево", хотя проблема в структуре экспорта.

Production pitfalls: типичные сбои в реальных проектах

1. Неправильный sideEffects

В package.json можно указать sideEffects: false, чтобы помочь сборщику. Ошибка в том, что эту настройку иногда ставят механически. Если в пакете есть CSS-импорты, полифилы, регистрация кастомных элементов или глобальные патчи, сборщик может выбросить то, что считалось обязательным.

Признаки:

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

2. Переход через CommonJS-обертку

Команда импортирует библиотеку как будто она ESM-friendly, но под капотом сборщик проваливается в CommonJS-слой. В метриках это видно по тяжелому vendor chunk, а в анализаторе по крупному пакету без точного разделения по exports.

Признаки:

  • одна функция тянет весь пакет;
  • vendor chunk почти не уменьшается после удаления явных импортов;
  • анализ показывает обобщенный модуль вместо отдельных файлов.

3. Barrel-файлы в shared-слое

Это не абстрактная "плохая практика", а частый источник регрессий. Особенно в дизайн-системах, где к простым компонентам примешаны иконки, токены, адаптеры для аналитики и старые утилиты.

Признаки:

  • любой экран импортирует один и тот же толстый shared chunk;
  • изменение малой утилиты инвалидирует кэш большого числа страниц;
  • bundle diff после маленького PR неожиданно большой.

Чтобы такие проблемы ловить заранее, удобно сочетать size-limit, bundle analyzer и профилирование реальных загрузок. Здесь хорошо дополняет тему статья про поиск узких мест через React performance profiling.

Прокачай React за 7 дней

20 вопросов и разборов по React Hooks.

Начать

Разбор производительности: где выигрыш оправдан, а где нет

Tree shaking не всегда дает заметный пользовательский эффект. Если вы вырезали 8 КБ из редко загружаемого чанка админки, цифра в отчете приятная, но поведение приложения с точки зрения бизнеса могло не измениться. Если же удалось сократить стартовый клиентский бандл публичной страницы на 60-80 КБ, это уже влияет на parse/execute time и на скорость первого взаимодействия, особенно на слабых устройствах.

Стоит смотреть на три уровня:

  • размер исходного чанка;
  • размер после gzip или brotli;
  • время парсинга и исполнения в браузере.

Частая ошибка команды в том, что она оптимизирует только transfer size. Но JavaScript требует ресурсов не только при передаче по сети. Его еще нужно распарсить, скомпилировать и выполнить. Поэтому tree shaking особенно полезен там, где удаляется код с тяжелой инициализацией, а не только строковые константы и маленькие утилиты.

С другой стороны, бывает и premature optimization. Если код уже разбит по роутам, а проблема приложения в лишних ререндерах, бессмысленно неделю бороться за 5 КБ, игнорируя когда компонент действительно перерисовывается в React. Оптимизация должна идти от узкого места, а не от красивого термина.

Практики, которые реально помогают

Держите exports явными

Лучше:

export { Button } from "./Button";
export { Input } from "./Input";

Хуже:

export * from "./Button";
export * from "./Input";
export * from "./legacy";

Явный экспорт проще анализировать и проще ревьюить.

Разделяйте чистые модули и модули инициализации

Если модуль что-то регистрирует, патчит или логирует при импорте, не смешивайте его с утилитами и компонентами. Инициализация должна жить отдельно, чтобы не тащить ее в каждый consumer.

Импортируйте из узкой точки, а не из "всего пакета"

Если библиотека позволяет, лучше брать конкретный модуль, а не корневой namespace. Это особенно важно для иконок, утилит дат и форматтеров.

Проверяйте прод-сборку, а не dev-режим

Tree shaking почти всегда оценивают по production build. Dev-сервер может держать граф иначе, без агрессивной оптимизации.

Согласовывайте правила с авторами внутренних пакетов

Если у вас монорепозиторий, tree shaking ломается не только в приложении, но и на границах пакетов (package boundaries). Внутренние библиотеки должны публиковать ESM-сборку, корректный exports map и аккуратный sideEffects.

Частые ошибки

1. Путать tree shaking и lazy loading

Первая техника удаляет лишнее из чанка. Вторая откладывает загрузку чанка. Они часто идут вместе, но не заменяют друг друга.

2. Верить, что любой import { x } уже гарантирует оптимизацию

Если пакет собран в CommonJS или корневой модуль содержит побочные эффекты, красивый синтаксис не спасет.

3. Ставить sideEffects: false без аудита пакета

Это опасно. Можно выиграть в размере и сломать поведение.

4. Игнорировать внутренние barrel-файлы

Многие команды подозревают только сторонние зависимости, хотя лишний код часто приезжает из собственного shared-слоя.

5. Считать gzip-size единственной метрикой

Для React-клиента важны еще parse time, execute time и стабильность кэша между релизами.

Как отвечать на интервью

Сильный ответ на вопрос про tree shaking в React обычно строится так:

  1. Дать короткое определение: это удаление неиспользуемого кода из итоговой сборки.
  2. Сразу назвать условия: ESM, отсутствие побочных эффектов, предсказуемый граф импортов.
  3. Показать ограничение: barrel-файлы, CommonJS, глобальная инициализация и неверный sideEffects снижают эффект.
  4. Развести понятия: tree shaking уменьшает содержимое чанка, code splitting меняет стратегию загрузки.
  5. Добавить production-проверку: bundle analyzer, сравнение чанков, проверка конкретных модулей в build output.

Хорошая формулировка звучит примерно так: "В React-проекте tree shaking полезен, когда модульный граф прозрачен. Если компоненты импортируются через чистые ESM-модули без сайд-эффектов, сборщик выкинет неиспользуемые exports. Но если у вас barrel-файлы, CommonJS-зависимости или код с инициализацией при импорте, эффект быстро падает. Поэтому я всегда проверяю итог на production build, а не верю в оптимизацию по названию флага".

Такой ответ показывает не только знание термина, но и инженерную точность. По той же логике на React-интервью обычно хорошо заходят темы про memoization и реальную оптимизацию и React optimization questions уровня middle.

Потренируйте React-собеседование на задачах про производительность

В Lexicon можно прогнать mock interview по React, разобрать вопросы про bundling, rendering и performance trade-offs и увидеть, где ответ пока звучит слишком поверхностно.

Начать практику

FAQ

Можно ли считать tree shaking заменой ручной оптимизации импортов

Нет. Он помогает только там, где граф зависимостей уже позволяет безопасное удаление кода. Плохая структура модулей не становится хорошей автоматически.

Почему lodash, иконки или UI-kit часто мешают уменьшить бандл

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

Нужен ли tree shaking, если приложение уже использует route-based splitting

Да, потому что даже внутри отдельного чанка можно держать лишний код. Но сначала стоит понять, какой именно чанк тормозит загрузку и рендеринг.

Какой минимальный набор проверок стоит добавить в командный процесс

Bundle analyzer, сравнение размеров чанков в CI, аудит sideEffects во внутренних пакетах и правило не смешивать инициализацию с экспортом компонентов.

Есть ли смысл оптимизировать маленький internal dashboard

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

Итоги

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

Если нужен практический ориентир, задайте себе три вопроса. Видит ли сборщик точные границы модулей. Безопасно ли удалить конкретный импорт. Подтверждается ли выигрыш реальным анализом production-сборки. Когда на все три вопроса есть уверенный ответ, tree shaking перестает быть красивым словом из статьи и начинает реально уменьшать бандл.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Автор

Lexicon Team

Читайте также