Раздел 7 · Браузер и Frontend

Бандлеры внутри: Vite, esbuild, SWC, свой за 80 строк

middle-senior~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Бандлеры внутри: Vite, esbuild, SWC, свой за 80 строк

Каждый раз, когда ты пишешь import { useState } from 'react', между этой строкой и работающей страницей стоит инструмент, который превращает десятки файлов в один или несколько JS-файлов для браузера. Этот инструмент называется бандлером. Большую часть карьеры с ним достаточно молча соглашаться, но в трёх случаях знание устройства спасает: когда страница долго грузится, когда бандл-аналитика показывает мегабайт там, где должно быть 200 КБ, и когда команда выбирает стек на новый проект. Этот урок разбирает, что делает бандлер, чем отличаются Vite, esbuild, SWC и Rspack, и пишет собственный маленький бандлер примерно на 80 строк, чтобы внутреннее устройство стало осязаемым.

1. Зачем бандлер

Браузер умеет грузить <script type="module"> и понимает ESM-импорты. Зачем тогда нужен бандлер?

  • Граф зависимостей. Бандлер строит граф модулей из entry, рекурсивно тянет импорты, и подаёт их браузеру одним или несколькими запросами вместо сотен.
  • Tree-shaking. Если ты импортировал из lodash одну функцию chunk, в bundle уезжает только она, а не весь lodash. Браузер этого не умеет.
  • Транспиляция. TS, JSX, последние ES-фичи превращаются в код, который поймёт прицельный browserslist. Сам браузер JSX и TS не понимает.
  • Code splitting. Граф режется на чанки по dynamic-import или маршрутам, лишний код не доезжает на первую страницу.
  • Asset pipeline. Картинки, шрифты, CSS-модули попадают в граф вместе с JS, получают хэши, версионируются.
  • Source maps. Bundled-позиция сопоставляется с исходной, чтобы стектрейс показывал твой код, а не минифицированную кашу.

Что взять с собой

  • Бандлер собирает граф модулей и режет его на чанки.
  • Tree-shaking, транспиляция, asset pipeline и source maps это его же работа.
  • Браузер ESM умеет, но без бандлера нет ни tree-shaking, ни code splitting.

2. Модульный граф и code splitting

Поиграй с маленьким графом Echo. Видно, как dynamic-import (lazy-ребро) делит граф на два чанка и сколько ты экономишь на первой загрузке.

В графе видна суть code splitting: лазейное ребро (import() или React.lazy) превращает поддерево в отдельный чанк. Всё, что доступно только через это ребро, не попадает в первый запрос. В нашем примере SearchPage со своим VirtualScroller и date-fns весят суммарно 49 КБ это почти половина общего бандла. Без splitting эти 49 КБ грузятся всегда, даже если пользователь не зашёл на /search.

Tree-shaking это сосед code splitting: он работает уже внутри одного чанка. Из условного import { chunk } from 'lodash-es' в бандл попадёт только функция chunk и её транзитивные зависимости, остальной lodash вырежется. Условия два: ESM-семантика (статические импорты, без require) и пометка sideEffects в package.json пакета. CommonJS-пакеты тришейкятся плохо.

Что взять с собой

  • Граф модулей режется на чанки по dynamic-import и маршрутам.
  • Lazy-ребро уносит поддерево в отдельный чанк, экономя первый запрос.
  • Tree-shaking вырезает неиспользуемое внутри чанка, требует ESM и sideEffects.

3. Vite: ESM в dev, Rollup в prod

Vite устроен необычно: разделил dev и prod. В разработке Vite вообще не бандлит. Он поднимает маленький dev-сервер, который отдаёт каждый твой файл браузеру отдельным ESM-модулем. Браузер сам выстраивает граф через <script type="module">. Изменил файл, Vite пересылает только его, горячая замена работает мгновенно.

В проде Vite зовёт Rollup: вот тут уже честное построение графа, tree-shaking, code splitting, минификация. Получается лучшее с обеих сторон: быстрый dev и зрелый prod.

// vite.config.ts
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
      },
    },
  },
});

manualChunks: { vendor: ['react', 'react-dom'] } создаст отдельный vendor-чанк это редко меняющийся код, его браузер закэширует надолго. А твой рабочий код в main будет инвалидироваться чаще, не утаскивая за собой React. Стандартный приём, чтобы CDN-кэш работал нормально.

Что взять с собой

  • В dev Vite не бандлит, отдаёт модули браузеру напрямую как ESM.
  • В prod использует Rollup: tree-shaking, code splitting, минификация.
  • manualChunks выносит редко меняющийся код в отдельный vendor-чанк.

4. Компиляторы: esbuild, SWC, Rust-семья

Внутри любого бандлера есть транспилятор: то, что превращает TSX в JS, и старый ES в современный. Они и определяют, насколько быстрая будет сборка.

  • esbuild написан на Go, делает почти всё в параллель. Vite использует его для pre-bundling зависимостей (CommonJS в ESM, объединение мелких модулей).
  • SWC это Rust-транспилятор, заменивший Babel в Next.js и Parcel 2. Поддерживает плагины (как Babel), быстрый, есть интеграции с Vite и Webpack.
  • Rspack и Turbopack это Rust-бандлеры с Webpack-совместимым API. Rspack используют, когда есть большой проект на Webpack и хочется ускорить сборку в десятки раз без переписывания конфига. Turbopack пишут авторы Next.js, идёт как dev-режим в Next 15+.

Общая черта последних пяти лет: тяжёлая работа переехала из JavaScript на Go и Rust. Сборка большого проекта раньше занимала минуты, теперь секунды.

Что взять с собой

  • Внутри бандлера всегда есть транспилятор, на нём держится скорость.
  • esbuild на Go и SWC на Rust в десятки раз быстрее Babel.
  • Rspack и Turbopack это Webpack-совместимые Rust-замены для legacy.

5. Webpack: где ещё актуален

Webpack много лет был стандартом, но сейчас уступил Vite в новых проектах. Знать его всё ещё стоит по двум причинам.

Первая: большая часть существующих frontend-проектов на Webpack, и они не переписываются за неделю. Loader-ы (babel-loader, css-loader), plugin-ы (html-webpack-plugin, mini-css-extract-plugin), модель чанков и splitChunksPlugin это рабочий инструмент, на котором живёт корпоративный фронт.

Вторая: Module Federation это уникальная функция Webpack 5 (Rspack теперь тоже умеет), на которой строят микрофронтенды. Полноценной замены пока нет.

Что взять с собой

  • Webpack стандарт уходит, но legacy-проекты на нём ещё долго.
  • Loader-ы и plugin-ы Webpack стоит уметь читать, даже не выбирая его новым.
  • Module Federation для микрофронтендов это уникальная фича Webpack/Rspack.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.