Как работает pre-bundling зависимостей

Одной из ключевых особенностей Vite является высокая скорость запуска dev-сервера. Эта скорость достигается не только за счёт использования native ES Modules в браузере, но и благодаря механизму предварительной обработки зависимостей — pre-bundling.

Pre-bundling — это этап предварительной сборки npm-зависимостей перед запуском приложения в режиме разработки. Во время этого процесса Vite анализирует подключённые пакеты из node_modules, преобразует их в оптимизированный формат и кэширует результат.

Основные задачи pre-bundling:

  • ускорение загрузки dev-сервера;
  • уменьшение количества HTTP-запросов;
  • преобразование CommonJS в ES Modules;
  • стабилизация импорта зависимостей;
  • оптимизация повторных запусков проекта.

Почему браузерные ES Modules создают проблему

Современные браузеры умеют работать с ES Modules напрямую:

import { createApp } from 'vue'

Однако при использовании npm-зависимостей возникает несколько сложностей.

Большое количество внутренних модулей

Некоторые библиотеки состоят из сотен или тысяч файлов.

Например:

import lodash from 'lodash'

Внутри lodash может существовать огромное количество импортов:

import baseClone from './baseClone.js'
import Stack from './Stack.js'
import isArray from './isArray.js'

Если браузер будет загружать каждый файл отдельно:

  • возрастает число HTTP-запросов;
  • увеличивается время анализа модулей;
  • dev-сервер начинает работать медленнее.

Проблема CommonJS

Многие старые npm-пакеты написаны в формате CommonJS:

const axios = require('axios')

module.exports = axios

Браузер не понимает CommonJS напрямую.

Он ожидает ES Modules:

import axios from 'axios'

Следовательно, Vite должен:

  1. обнаружить CommonJS-пакет;
  2. преобразовать его в ESM;
  3. подготовить совместимую версию для браузера.

Именно этим занимается pre-bundling.


Как Vite запускает pre-bundling

При первом запуске:

npm run dev

Vite выполняет несколько этапов.

1. Сканирование исходного кода

Сначала анализируются импортируемые зависимости:

import vue from 'vue'
import axios from 'axios'
import dayjs from 'dayjs'

Vite строит граф зависимостей и определяет:

  • какие пакеты используются;
  • какие пакеты находятся в node_modules;
  • какие из них требуют оптимизации.

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

Для pre-bundling Vite использует esbuild.

esbuild выполняет:

  • сверхбыстрый анализ модулей;
  • преобразование CommonJS;
  • объединение мелких модулей;
  • генерацию ESM-версий.

Пример условного преобразования:

Исходный пакет:

const lib = require('./lib')

module.exports = {
    lib
}

После обработки:

import lib from './lib.js'

export {
    lib
}

Почему выбран esbuild

esbuild написан на Go и работает значительно быстрее большинства JavaScript-бандлеров.

Скорость особенно важна именно для dev-режима, где критичны:

  • мгновенный запуск;
  • быстрый HMR;
  • минимальная задержка обновлений.

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


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

После обработки Vite создаёт специальный кэш:

node_modules/.vite

Пример содержимого:

node_modules/.vite/deps/

Там находятся:

  • оптимизированные ESM-файлы;
  • metadata-файлы;
  • карта зависимостей;
  • кэшированные версии модулей.

Пример:

chunk-ABCD1234.js
vue.js
axios.js
_metadata.json

Как работает кэширование

После первой оптимизации Vite не запускает pre-bundling повторно без необходимости.

При следующем запуске:

npm run dev

Vite проверяет:

  • изменился ли package.json;
  • изменился ли lock-файл;
  • появились ли новые зависимости;
  • обновились ли версии пакетов.

Если изменений нет — используется готовый кэш.

Это резко ускоряет повторный старт dev-сервера.


Что находится внутри _metadata.json

Vite хранит служебную информацию о зависимостях.

Пример структуры:

{
  "optimized": {
    "vue": {
      "src": "../. ./vue/dist/vue.runtime.esm-bundler.js",
      "file": "vue.js"
    }
  }
}

Этот файл позволяет:

  • быстро находить оптимизированные версии;
  • избегать повторной обработки;
  • отслеживать изменения зависимостей.

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

По умолчанию Vite автоматически анализирует импорты.

Например:

import react from 'react'
import reactDOM from 'react-dom'

Эти пакеты будут автоматически pre-bundled.


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

Vite также способен анализировать некоторые динамические импорты:

const module = await import('./module.js')

Но динамические зависимости сложнее для анализа.

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


Настройка optimizeDeps

Для управления pre-bundling используется раздел:

export default defineConfig({
    optimizeDeps: {

    }
})

optimizeDeps.include

Позволяет принудительно добавить пакет в pre-bundling.

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

Полезно в случаях:

  • скрытых импортов;
  • dynamic import;
  • нестандартных зависимостей;
  • монорепозиториев.

optimizeDeps.exclude

Исключает пакет из pre-bundling.

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

Используется редко, но бывает полезно:

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

Как Vite объединяет зависимости

Предположим, библиотека содержит 500 внутренних модулей.

Без pre-bundling браузер выполнит:

  • 500 импортов;
  • 500 запросов;
  • 500 операций парсинга.

После pre-bundling:

lodash.js

Браузер загружает один оптимизированный файл вместо сотен мелких модулей.


Влияние на HMR

Hot Module Replacement напрямую зависит от скорости обработки модулей.

Если зависимости уже оптимизированы:

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

Разница между dev и production

Важно понимать, что pre-bundling работает только в dev-режиме.

Dev

Используется:

  • native ESM;
  • esbuild;
  • optimizeDeps;
  • pre-bundling.

Production

Используется production-сборка через Rollup:

vite build

В production:

  • создаётся полноценный bundle;
  • выполняется tree-shaking;
  • происходит code splitting;
  • минифицируются файлы.

Pre-bundling и production bundle — разные процессы.


Почему Vite не бандлит весь проект в dev-режиме

Традиционные bundler-подходы требуют полной пересборки приложения.

Например:

webpack -> bundle everything

Vite работает иначе:

  • исходный код остаётся отдельными ESM-модулями;
  • бандлятся только зависимости;
  • приложение обслуживается напрямую браузеру.

Это уменьшает объём работы dev-сервера.


Пример полного процесса

Исходный код

import React from 'react'
import ReactDOM from 'react-dom'
import axios from 'axios'

Этап 1 — анализ

Vite обнаруживает:

  • react
  • react-dom
  • axios

Этап 2 — pre-bundling через esbuild

Создаются:

react.js
react-dom.js
axios.js

Этап 3 — кэширование

Файлы сохраняются:

node_modules/.vite/deps/

Этап 4 — запуск dev-сервера

Браузер получает уже оптимизированные зависимости.


Что происходит при установке новой зависимости

После команды:

npm install dayjs

Vite обнаруживает изменение lock-файла.

При следующем запуске:

npm run dev

pre-bundling выполняется заново только для изменённых зависимостей.


Форсированная переоптимизация

Иногда требуется очистить кэш.

Для этого используется:

vite --force

Или:

npm run dev -- --force

Vite:

  • удаляет старый кэш;
  • повторно анализирует зависимости;
  • заново запускает pre-bundling.

Типичные причины проблем pre-bundling

Некорректные CommonJS-пакеты

Некоторые библиотеки имеют нестандартную структуру:

if (process.env.NODE_ENV) {
    require('./prod')
}

Статический анализ становится сложнее.


Dynamic require

Проблемный код:

require(someVariable)

esbuild не может заранее определить модуль.


Смешивание ESM и CommonJS

Некоторые библиотеки одновременно используют:

module.exports

и

export default

Это может вызывать ошибки совместимости.


Монорепозитории и pre-bundling

В monorepo-структурах зависимости могут находиться вне стандартного node_modules.

Пример:

packages/
apps/
shared/

В таких случаях Vite иногда требует ручного указания:

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

Почему pre-bundling особенно важен для React

Экосистема React традиционно содержит большое количество CommonJS-пакетов.

Например:

  • redux;
  • react-router;
  • older utility libraries;
  • legacy ecosystem packages.

Pre-bundling позволяет:

  • уменьшить задержку запуска;
  • стабилизировать импорт;
  • ускорить HMR.

Предварительная оптимизация и HTTP/2

Даже при использовании HTTP/2 большое количество модулей остаётся проблемой.

Причины:

  • браузеру нужно анализировать каждый файл;
  • возрастает CPU-нагрузка;
  • увеличивается время обработки dependency graph.

Pre-bundling снижает эти издержки.


Как Vite ускоряет повторный импорт

После оптимизации импорт:

import axios from 'axios'

фактически превращается во внутренний путь:

/node_modules/.vite/deps/axios.js

Браузер получает уже подготовленный ESM-файл.


Связь pre-bundling и dependency graph

Vite строит граф зависимостей приложения.

Pre-bundling уменьшает сложность этого графа:

500 внутренних модулей
↓
1 оптимизированный модуль

Это улучшает:

  • время старта;
  • скорость навигации;
  • HMR;
  • работу source maps.

Оптимизация cold start

Cold start — первый запуск dev-сервера.

Именно в этот момент pre-bundling даёт наибольший эффект.

Без оптимизации:

  • тысячи модулей;
  • медленный старт;
  • высокая нагрузка.

С pre-bundling:

  • минимальное число импортов;
  • быстрый старт;
  • готовый кэш.

Архитектурная роль pre-bundling

Pre-bundling является фундаментальной частью архитектуры Vite.

Именно он позволяет совместить:

  • native ESM;
  • высокую скорость dev-сервера;
  • поддержку legacy npm-пакетов;
  • мгновенный HMR;
  • минимальные задержки при разработке.

Без механизма предварительной оптимизации использование ES Modules в крупных frontend-проектах было бы значительно менее эффективным.