Pre-bundling: как и когда запускается

Pre-bundling — это этап предварительной сборки зависимостей, который выполняется Vite перед запуском dev-сервера. Главная задача механизма — ускорение загрузки приложения в браузере и уменьшение количества сетевых запросов при разработке.

Во время pre-bundling Vite анализирует импортируемые зависимости из node_modules, объединяет их в оптимизированные чанки и преобразует несовместимые форматы модулей в ESM.

Основной инструмент, выполняющий эту работу, — esbuild.


Зачем Vite нужен pre-bundling

Проблема большого количества модулей

Многие npm-пакеты состоят из сотен или тысяч файлов. Например:

import { debounce } from 'lodash-es'

Даже один импорт может подтягивать десятки внутренних модулей.

Без предварительной сборки браузер в режиме разработки будет выполнять огромное количество HTTP-запросов:

node_modules/
 ├── package/
 │   ├── module1.js
 │   ├── module2.js
 │   ├── module3.js
 │   └── ...

Это приводит к:

  • замедлению старта dev-сервера;
  • увеличению времени первой загрузки страницы;
  • перегрузке браузерного module graph;
  • проблемам с HMR.

Проблема CommonJS

Современный Vite работает через native ESM браузера:

import React from 'react'

Но значительная часть npm-экосистемы всё ещё использует CommonJS:

const React = require('react')
module.exports = React

Браузер не умеет напрямую исполнять CommonJS-модули.

Pre-bundling решает проблему:

  • преобразует CommonJS в ESM;
  • объединяет большое количество файлов;
  • генерирует оптимизированные dependency chunks.

Когда запускается pre-bundling

Первый запуск dev-сервера

Наиболее типичный сценарий:

vite

или:

npm run dev

Во время первого старта Vite:

  1. сканирует исходный код;
  2. ищет bare imports;
  3. определяет зависимости;
  4. запускает esbuild;
  5. сохраняет результат в cache.

После установки новых зависимостей

Если была установлена новая библиотека:

npm install axios

Vite обнаружит изменение dependency graph и повторно выполнит pre-bundling.


После удаления cache

Удаление cache также приводит к повторной оптимизации:

rm -rf node_modules/.vite

После следующего запуска dev-сервера pre-bundling будет выполнен заново.


После изменения lock-файла

Vite отслеживает:

  • package-lock.json
  • yarn.lock
  • pnpm-lock.yaml

Если lock-файл изменился, cache dependency optimization инвалидируется.


При изменении optimizeDeps

Изменение конфигурации:

export default defineConfig({
  optimizeDeps: {
    include: ['some-lib']
  }
})

также запускает повторную оптимизацию.


Как Vite определяет зависимости

Анализ импортов

Vite выполняет fast scan исходников.

Например:

import vue from 'vue'
import axios from 'axios'
import { map } from 'lodash-es'

Все bare imports считаются кандидатами для pre-bundling.


Bare imports

Bare import:

import React from 'react'

Не bare import:

import App from './App.vue'

Vite оптимизирует именно зависимости из node_modules.


Процесс pre-bundling по этапам

1. Dependency scan

Vite анализирует entry points приложения.

Обычно это:

index.html

или:

main.js
main.ts

Во время сканирования строится dependency graph.


2. Передача зависимостей в esbuild

После анализа Vite формирует список зависимостей:

[
  'react',
  'react-dom',
  'axios'
]

И передаёт их в esbuild.


3. Преобразование CommonJS

esbuild автоматически конвертирует:

const lib = require('lib')

в:

import lib from 'lib'

4. Объединение модулей

Множество внутренних файлов библиотеки объединяются в один оптимизированный chunk.

Например:

node_modules/.vite/deps/

5. Создание metadata

Vite создаёт metadata-файлы:

_metadata.json

В них хранится:

  • hash зависимостей;
  • информация о чанках;
  • карта оптимизации;
  • версии файлов.

Где хранится результат pre-bundling

По умолчанию:

node_modules/.vite

Пример:

node_modules/.vite/
 ├── deps/
 ├── _metadata.json
 └── package.json

Как выглядят оптимизированные зависимости

Исходный импорт:

import React from 'react'

После pre-bundling браузер получает:

import "/node_modules/.vite/deps/react.js"

Почему используется esbuild

Vite выбрал esbuild по нескольким причинам.

Очень высокая скорость

esbuild написан на Go и значительно быстрее:

  • webpack;
  • Babel;
  • Rollup;
  • Terser.

Быстрая конвертация CommonJS

esbuild эффективно преобразует:

  • CommonJS;
  • UMD;
  • legacy packages.

Минимальная задержка старта

Pre-bundling должен выполняться практически мгновенно.

Иначе пропадает основное преимущество Vite — быстрый cold start.


Dependency optimization cache

Механизм cache

После первой оптимизации Vite повторно использует уже собранные зависимости.

Это позволяет запускать dev-сервер почти мгновенно.


Что влияет на invalidation

Cache сбрасывается при:

  • обновлении пакетов;
  • изменении lock-файлов;
  • изменении vite config;
  • смене NODE_ENV;
  • изменении optimizeDeps.

optimizeDeps

Раздел конфигурации:

export default defineConfig({
  optimizeDeps: {

  }
})

Позволяет управлять pre-bundling вручную.


optimizeDeps.include

Принудительное включение зависимости:

export default defineConfig({
  optimizeDeps: {
    include: ['my-lib']
  }
})

Используется когда:

  • библиотека импортируется динамически;
  • Vite не смог обнаружить dependency;
  • пакет содержит нестандартные импорты.

optimizeDeps.exclude

Исключение зависимости:

export default defineConfig({
  optimizeDeps: {
    exclude: ['big-lib']
  }
})

Полезно:

  • для ESM-ready библиотек;
  • при проблемах совместимости;
  • для linked packages.

optimizeDeps.entries

Явное указание entry points:

export default defineConfig({
  optimizeDeps: {
    entries: ['./src/main.js']
  }
})

Позволяет контролировать dependency scan.


optimizeDeps.esbuildOptions

Передача параметров напрямую в esbuild:

export default defineConfig({
  optimizeDeps: {
    esbuildOptions: {
      target: 'es2020'
    }
  }
})

Dynamic imports и pre-bundling

Автоматическое обнаружение

Статические dynamic imports обычно обнаруживаются:

import('./module.js')

Ограничения

Но полностью динамические конструкции:

import(someVariable)

не всегда могут быть проанализированы.

В таком случае dependency приходится добавлять вручную:

optimizeDeps: {
  include: ['library']
}

Monorepo и pre-bundling

В monorepo возникают особенности.

Linked packages

Пакеты через:

pnpm workspace
npm link
yarn link

часто рассматриваются как source code, а не dependency.

Поэтому Vite может не выполнять pre-bundling автоматически.


Оптимизация linked packages

Иногда требуется:

optimizeDeps: {
  include: ['shared-package']
}

SSR и pre-bundling

Для SSR используется отдельная логика dependency optimization.

Некоторые зависимости:

  • externalized;
  • не бандлятся;
  • загружаются напрямую через Node.js.

CommonJS interop

Проблема named exports

CommonJS не поддерживает named exports нативно:

const lib = require('lib')

Но Vite создаёт ESM-обёртки:

import __cjs from '/dep.js'

export const something = __cjs.something

Почему иногда возникают ошибки pre-bundling

Нестандартные сборки библиотек

Некоторые пакеты:

  • используют runtime require;
  • генерируют импорты динамически;
  • имеют broken exports.

Это затрудняет анализ.


Ошибки export map

Некорректный package.json:

{
  "exports": {
    ".": "./wrong.js"
  }
}

может ломать dependency optimization.


Node-only зависимости

Пакеты, использующие:

fs
path
net

не могут работать в браузере.

Во время pre-bundling появляются ошибки.


Принудительный re-bundle

Флаг –force

vite --force

Игнорирует cache и выполняет dependency optimization заново.


Типичные случаи использования

  • повреждение cache;
  • обновление пакетов;
  • проблемы HMR;
  • смена branch;
  • обновление lock-файлов.

Отличие pre-bundling от production build

Dev optimization

Pre-bundling используется только для разработки.

Цели:

  • быстрый старт;
  • уменьшение запросов;
  • CommonJS compatibility.

Production build

Production build выполняет:

  • tree-shaking;
  • code splitting;
  • minification;
  • hashing;
  • оптимизацию assets.

Для production используется Rollup.


Взаимодействие pre-bundling и HMR

Предварительно оптимизированные зависимости редко изменяются.

Поэтому:

  • Vite не отслеживает их активно;
  • HMR работает быстрее;
  • уменьшается нагрузка на module graph.

Как понять, что pre-bundling выполняется

Во время старта dev-сервера:

Optimizing dependencies...

или:

Pre-bundling dependencies:

Логи optimized dependencies

Пример:

vite:deps Crawling dependencies...
vite:deps Dependencies bundled in 120ms

Как отключить pre-bundling

Полностью отключать механизм обычно не рекомендуется.

Но возможно:

export default defineConfig({
  optimizeDeps: {
    disabled: true
  }
})

Последствия отключения

Без pre-bundling:

  • растёт количество HTTP-запросов;
  • медленнее стартует dev-server;
  • ухудшается HMR;
  • CommonJS может перестать работать.

Особенности ESM-only пакетов

Современные ESM-пакеты:

export function test() {}

обычно требуют минимальной оптимизации.

Но Vite всё равно может объединять их для уменьшения количества запросов.


Browser cache и pre-bundling

Оптимизированные чанки получают hash:

react.js?v=f3sf2a

Это помогает:

  • избегать конфликтов cache;
  • быстро обновлять зависимости;
  • корректно инвалидировать старые версии.

Связь pre-bundling и dependency graph

Dependency graph Vite содержит:

  • source modules;
  • optimized deps;
  • HMR boundaries.

Pre-bundling уменьшает сложность графа, объединяя множество внутренних модулей библиотеки в один узел.


Архитектурная схема процесса

Исходный код
     ↓
Dependency scan
     ↓
Поиск bare imports
     ↓
esbuild
     ↓
CommonJS → ESM
     ↓
Bundling
     ↓
node_modules/.vite/deps
     ↓
Dev server

Наиболее частые сценарии проблем

Библиотека не оптимизируется

Причины:

  • dynamic import;
  • нестандартный export;
  • monorepo;
  • linked package.

Решение:

optimizeDeps: {
  include: ['package-name']
}

Ошибка stale cache

Симптомы:

  • старый код;
  • некорректный HMR;
  • missing export.

Решение:

vite --force

или:

rm -rf node_modules/.vite

Ошибка CommonJS compatibility

Некоторые старые библиотеки используют:

eval()
require(variable)

Такие конструкции плохо анализируются statically.


Роль pre-bundling в архитектуре Vite

Pre-bundling — один из ключевых механизмов, благодаря которым Vite обеспечивает:

  • быстрый cold start;
  • почти мгновенный HMR;
  • минимальную задержку обновлений;
  • эффективную работу с npm-экосистемой;
  • поддержку CommonJS и legacy packages;
  • высокую производительность dev-среды.

Без dependency optimization Vite потерял бы значительную часть преимуществ native ESM-подхода.