Браузер понимает HTML, CSS и JavaScript — но современный frontend-код пишется на TypeScript и JSX, разбивается на сотни модулей и зависит от десятков библиотек. Чтобы это стало тем, что понимает браузер, нужна сборка.
Зачем нужна сборка
Без неё несколько проблем сразу:
- TypeScript и JSX браузер не понимает — нужна трансформация в обычный JavaScript.
- Сотни модулей с
import/export— без объединения браузер делает сотни отдельных HTTP-запросов, что медленно. - Оптимизации — минификация, tree shaking (удаление неиспользуемого кода), хэширование имён файлов для долгосрочного кеша — вручную не масштабируется.
Бандлер делает всё это: принимает точку входа, строит граф зависимостей, трансформирует и объединяет модули, отдаёт готовые файлы.
Что происходит внутри бандлера
Бандлер начинает с точки входа — обычно src/main.tsx. Читает её импорты, находит каждый файл, читает его импорты и так далее. Получается граф модулей:
main.tsx
└─ App.tsx
├─ pages/HomePage.tsx
│ └─ components/Hero.tsx
└─ pages/ProductPage.tsx
└─ api/products.ts
Из этого графа бандлер собирает один или несколько выходных файлов. Попутно он трансформирует TypeScript в JS, JSX в вызовы React.createElement, применяет минификацию и прочие оптимизации.
Инструменты сегодня
Vite — стандарт для новых проектов. В режиме разработки он не собирает полный бандл: браузеру отдаются отдельные ESM-модули с трансформацией через esbuild — запуск мгновенный. В продакшене Vite использует Rollup для оптимизированной сборки. Для старта нового проекта — npm create vite@latest.
webpack — был стандартом годами; встречается в существующих проектах и в Create React App. Медленнее Vite, но экосистема плагинов зрелая. Новый проект на webpack начинать не стоит.
esbuild и Rollup работают как движки под другими инструментами: esbuild — в Vite для dev-режима и трансформации, Rollup — для prod-сборки. Напрямую их конфигурируют редко.
Rspack (Rust-порт webpack) и Turbopack (Vercel) — альтернативы с фокусом на скорость. Актуальны для очень больших монорепозиториев; для среднего проекта разница незаметна.
Dev-режим vs продакшен-сборка
Это два принципиально разных режима, которые решают разные задачи.
Dev-режим — для скорости итерации:
- HMR (Hot Module Replacement) обновляет изменённый модуль в браузере без перезагрузки страницы; состояние компонента сохраняется.
- Нет минификации — код читаемый, отладка в DevTools работает нормально.
- Source maps включены по умолчанию.
Продакшен-сборка — для пользователя:
- Минификация уменьшает объём в 3–5 раз: убираются пробелы, переименовываются переменные.
- Хэшированные имена файлов (
app.a8f3c2.js) позволяют браузеру кешировать файл бессрочно — при изменении хэш меняется и браузер загружает новую версию. - Tree shaking и code splitting активны.
vite # dev-сервер
vite build # продакшен-сборка в dist/
vite preview # локальный просмотр dist/ перед деплоем
Code splitting и динамический import
По умолчанию весь код приложения попадает в один файл. Пользователь скачивает код страницы настроек, даже если зашёл только на главную.
Code splitting разбивает бандл на части. Самая естественная граница — маршруты:
import { lazy, Suspense } from 'react';
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Suspense>
);
}
React.lazy + динамический import() — бандлер создаёт отдельный чанк для SettingsPage. Он загружается только когда пользователь переходит на /settings. Начальный файл стал меньше — страница открывается быстрее.
Suspense здесь — декларативная граница загрузки: пока чанк не загрузился, пользователь видит <Spinner />. Подробнее о его роли в обработке состояний — в статье «Обработка ошибок и Suspense».
Tree shaking
Если импортировать одну функцию из большой библиотеки, tree shaking гарантирует, что в бандл попадёт только она, а не вся библиотека:
import { format } from 'date-fns'; // в бандл попадёт только format
Tree shaking работает только с ESM (import/export). CommonJS (require) непрозрачен для статического анализа — бандлер не может знать, что из модуля используется, и включает его целиком. Это главная причина переехать на ESM-версии библиотек.
Вторая ловушка — сайд-эффекты. Если модуль делает что-то при импорте (регистрирует глобальные полифиллы, добавляет плагины), бандлер обязан его включить. Библиотека должна объявить "sideEffects": false в package.json, иначе tree shaking её не тронет.
Анализ размера бандла
Если начальная загрузка медленная — смотрите, что в бандле. Для Vite есть плагин rollup-plugin-visualizer:
npm install -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [visualizer({ open: true })],
};
После vite build откроется карта модулей с визуальным распределением размеров. Типичные причины тяжёлого бандла:
- Тяжёлые зависимости — moment.js с локалями (~2 МБ), lodash целиком. Заменить на
date-fnsи отдельные функцииlodash-es. - Дубликаты — одна библиотека затянула другую версию той же зависимости. Решается через
npm dedupeилиresolve.aliasв конфигурации Vite. - Отсутствие code splitting — весь код в одном файле.
Переменные окружения и режимы
Vite читает .env-файлы и предоставляет переменные через import.meta.env. В бандл попадают только переменные с префиксом VITE_:
# .env.production
VITE_API_URL=https://api.example.com
const apiUrl = import.meta.env.VITE_API_URL;
Переменные без VITE_ недоступны в клиентском коде. Это защита от случайного попадания секретов в браузерный бандл.
Чек-лист здоровой сборки
- Продакшен-сборка проходит без предупреждений — предупреждения Rollup и TypeScript это сигналы, не шум.
- Размер начального чанка — до 200 КБ gzip для типового приложения; больше — смотрите в visualizer.
- Code splitting по маршрутам — каждый крупный маршрут в отдельном чанке через
React.lazy. - Source maps загружены в мониторинг — без них стек ошибки в продакшене нечитаем.
- Зависимости ESM, а не CommonJS — tree shaking работает только на ESM.
.env-файлы в.gitignore, толькоVITE_-переменные в коде — секреты не попадают в репозиторий.
Коротко
- Бандлер трансформирует TypeScript/JSX, строит граф модулей и выдаёт оптимизированные файлы для браузера.
- Vite — стандарт для новых проектов: мгновенный dev-сервер на ESM + оптимизированная prod-сборка через Rollup.
- Dev-режим и продакшен решают разные задачи: HMR и читаемый код vs минификация и хэширование файлов.
React.lazy+ динамическийimport()— code splitting по маршрутам без лишней настройки.- Tree shaking удаляет неиспользуемый код, но только из ESM-библиотек; CommonJS включается целиком.
- Visualizer покажет, что занимает место; частые виновники — тяжёлые зависимости и дубликаты.
- Только
VITE_-переменные попадают в клиентский код — секреты без этого префикса не утекут.
Что почитать дальше
- Производительность рендеринга — code splitting с
React.lazyи оптимизация компонентов. - Модули и npm — как устроены ES-модули и
package.json. - Инструменты TypeScript и Node — tsconfig, линтеры и окружение разработчика.
- Обработка ошибок и Suspense — Suspense как граница загрузки при code splitting.