Зачем вообще сборщик
Ты пишешь сотни модулей на TypeScript и JSX, импортируешь CSS, картинки и пакеты из node_modules. Браузер так не умеет: TS и JSX он не понимает, голые импорты вида import React from 'react' без import map не резолвит, а сотни мелких файлов грузить долго. Сборщик:
- строит граф зависимостей от точки входа (
main.tsx); - трансформирует файлы: TS и JSX в JS, современный синтаксис под целевые браузеры, CSS через PostCSS;
- склеивает модули в несколько чанков и минифицирует их;
- выдаёт файлы с хешем содержимого в имени (
index-3f9a1c.js), чтобы их можно было кешировать навсегда.
Vite и webpack
webpack собирает весь граф и в dev, и в проде. Всё настраивается через loaders (как превратить файл в модуль: ts-loader, css-loader) и plugins (что сделать с результатом: HTML, минификация, env). Гибкий, но на больших проектах dev-сервер стартует медленно.
Vite в dev вообще не бандлит твой код: браузер получает нативные ES-модули, Vite трансформирует каждый файл по запросу. Зависимости из node_modules один раз предварительно собираются (pre-bundling), чтобы не грузить тысячи файлов. Поэтому старт почти мгновенный. Для продакшена Vite делает полноценную сборку бандлером (исторически Rollup, в новых версиях Rolldown).
HMR (hot module replacement): при правке файла заменяется только он и его зависимые модули, страница не перезагружается, состояние React-компонентов обычно сохраняется.
Tree shaking
Удаление неиспользуемого кода. Работает, потому что import/export в ES-модулях статические: сборщик видит, что из lodash-es взяли только debounce, и выкидывает остальное.
// хорошо: статический именованный импорт, лишнее вырежется
import { debounce } from 'lodash-es';
// плохо: CommonJS, сборщик не знает, что реально используется
const _ = require('lodash');
Мешают tree shaking: CommonJS, побочные эффекты на уровне модуля (код, который выполняется при импорте), реэкспорт всего через export * в библиотеках без пометки. Поле "sideEffects": false в package.json библиотеки говорит сборщику: модули без использованных экспортов можно выбрасывать целиком.
Code splitting
Динамический import() создаёт отдельный чанк, который грузится по требованию:
const Editor = lazy(() => import('./Editor'));
Так режут бандл по маршрутам и тяжёлым виджетам. Общие зависимости сборщик выносит в общие чанки, чтобы не дублировать.
Транспиляция и типы
esbuild и SWC превращают TS в JS очень быстро, потому что просто вырезают типы и не проверяют их. Проверку типов делают отдельно: tsc --noEmit в CI или в редакторе. Поэтому «собралось» не значит «типы правильные».
Source maps
Карта соответствия минифицированного кода исходникам. Нужна для отладки и для стектрейсов в Sentry. В проде их часто не отдают публично, а загружают в систему мониторинга ошибок.
Переменные окружения
В браузерный бандл значения окружения вшиваются на этапе сборки. В Vite в клиентский код попадают только переменные с префиксом VITE_ (import.meta.env.VITE_API_URL). Всё, что попало в бандл, видно любому пользователю, секретам там не место.
Частые ошибки
- Импорт всей библиотеки ради одной функции.
- Ждать, что esbuild поймает ошибку типов.
- Класть секреты в переменные, доступные клиенту.
- Отдавать
index.htmlс долгим кешем: тогда пользователи не увидят новые хеши файлов.
Что спросят на собесе
- Зачем нужен сборщик и что он делает с кодом.
- Почему Vite быстрее webpack в разработке.
- Что такое tree shaking и что ему мешает.
- Как работает code splitting и ленивые импорты.
- Зачем хеш в имени файла и что такое source maps.