Vite 05 ⚡ Dependency Pre-Bundling — Why node_modules gets bundled separately
The dev server serves the source code on demand, but it does not serve the node_modules the same way. The dependencies are pre-bundled once at startup, and the browser receives a single merged file per dependency instead of the library’s internal module tree. The previous chapter covered the on-demand transform; this chapter explains the exception that makes the on-demand model practical. The pre-bundling is the mechanism that solves two problems: the CommonJS compatibility and the request flood.
The first problem is the module format. Vite serves all code as native ESM during development, but many dependencies still ship as CommonJS or UMD. The browser cannot import these formats, so Vite must convert them into ESM before the browser sees them . The second problem is the request count. Some libraries ship as many separate ESM files, and lodash-es is the canonical example: it has over 600 internal modules. A single import { debounce } from 'lodash-es' would cause the browser to fire more than 600 HTTP requests at the same time, which creates network congestion and noticeably slows the page load . The pre-bundling merges those 600 modules into one file, and the browser makes a single request instead .
Key point: The dependency pre-bundling only applies in development mode. The source code is never pre-bundled; it is served on demand. The production build uses a different bundler for the optimized output, and the pre-bundling cache is stored in node_modules/.vite with a content hash that invalidates when the dependencies or the configuration change .
Why the dependencies require separate treatment
The CommonJS problem.
A CommonJS dependency uses module.exports and require(), which are not part of the browser’s ESM specification. The browser cannot execute the require() calls, and it cannot resolve the module.exports object. The pre-bundling converts the CommonJS into ESM, and it performs the smart import analysis so that the named imports work even when the exports are dynamically assigned, such as React’s import React, { useState } from 'react' . Without the conversion, the dependency cannot be loaded at all.
The request-flood problem.
An ESM dependency that ships as many separate files is technically loadable by the browser, but the request count makes it impractical. The browser’s connection limit per host means that the 600 requests for lodash-es are queued, and the page waits for the queue to drain. The pre-bundling merges the internal modules into one file, and the browser makes one request instead of 600 . The pre-bundling is the performance optimization that makes the ESM dependencies practical.
The cache problem.
The pre-bundled files are cached with a content hash in the URL. The hash changes when the lockfile, the dependency versions, the optimizeDeps configuration, or the Vite version changes . The browser caches the pre-bundled files indefinitely with the Cache-Control: max-age=31536000,immutable header, so the reload does not re-fetch the unchanged dependencies . The cache is the mechanism that keeps the subsequent page loads fast.
a. The automatic discovery and the pre-bundling
The pre-bundling runs automatically when the dev server starts for the first time, or when the cache is missing. Vite crawls the source code, discovers the bare imports, and uses them as the entry points for the pre-bundle .
# Start the dev server for the first time
npm run dev
# Output:
# Pre-bundling dependencies:
# react
# react-dom
# (this will be run only when your dependencies or config have changed)
The crawl ignores the node_modules, the build.outDir, the __tests__, and the coverage directories by default . The discovered imports are the bare specifiers that expect to be resolved from node_modules, and the pre-bundling runs with Rolldown in Vite 8 and with esbuild in the earlier versions . The pre-bundling is fast, and the output is the set of the merged dependency files in node_modules/.vite/deps.
If a new dependency import is encountered after the server has started, and it is not already in the cache, Vite re-runs the pre-bundling and reloads the page if needed .
b. The optimizeDeps configuration
The automatic discovery does not always find every dependency. A dependency that is imported only at runtime, or one that is created by a plugin transform, is not visible during the initial crawl. The optimizeDeps.include option forces the pre-bundling of the specific dependency.
// vite.config.ts
export default defineConfig({
optimizeDeps: {
include: ['echarts/core'], // Pre-bundle this runtime-discovered dependency
exclude: ['@my/utils'], // Skip the pre-bundling for a local ESM package
},
})
The include option is for the large or CommonJS dependencies that the crawl missed. The exclude option is for the small ESM packages that are already valid ESM and have few internal modules, where the pre-bundling would add an unnecessary layer . The CommonJS dependencies should not be excluded, because the browser cannot execute them without the conversion .
The force option re-runs the pre-bundling even when the cache exists.
optimizeDeps: {
force: true, // Re-bundle the dependencies, ignoring the cache
}
The force option is the mechanism for the cases where the cache is stale but the lockfile has not changed, such as when the linked dependency changes in a monorepo .
c. The cache and the invalidation
The pre-bundled dependencies are stored in node_modules/.vite, and the cache is invalidated when the lockfile, the patches, the vite.config.js fields, or the NODE_ENV changes .
# The cache directory
ls node_modules/.vite/deps/
# The pre-bundled files with the content hash in the filename
The cache is the mechanism that keeps the dev server startup fast. The browser caches the pre-bundled files indefinitely, and the server does not re-process them unless the invalidation condition is met. The --force flag is the manual override when the automatic invalidation misses the change .
Complete Example Session
# ============================================
# PART 1: START THE DEV SERVER
# ============================================
npm run dev
# The first run pre-bundles the dependencies
# ============================================
# PART 2: OBSERVE THE PRE-BUNDLING MESSAGE
# ============================================
# The terminal shows the pre-bundled dependencies
# ============================================
# PART 3: CHECK THE CACHE
# ============================================
ls node_modules/.vite/deps/
# The merged dependency files
# ============================================
# PART 4: THE BARE-IMPORT REWRITE
# ============================================
# The browser requests /node_modules/.vite/deps/react.js
# instead of the react package's internal modules
# ============================================
# PART 5: THE COMMONJS CONVERSION
# ============================================
# A CommonJS dependency is converted to ESM
# The named imports work as expected
# ============================================
# PART 6: THE CACHE INVALIDATION
# ============================================
# Change a dependency version and run npm install
# The pre-bundling re-runs with the new hash
# ============================================
# PART 7: THE FORCE RE-BUNDLE
# ============================================
npm run dev -- --force
# The pre-bundling runs again, ignoring the cache
The seven parts demonstrate the pre-bundling lifecycle: the automatic run, the message, the cache, the rewrite, the CommonJS conversion, the invalidation, and the force re-bundle.
Quick Reference
The Pre-Bundling Purpose
| Problem | Solution |
|---|---|
| CommonJS/UMD compatibility | Convert to ESM |
| Request flood (many internal modules) | Merge into one file |
| Cache efficiency | Content hash in the URL |
The optimizeDeps Options
| Option | Purpose |
|---|---|
include | Force the pre-bundling of a dependency |
exclude | Skip the pre-bundling for a local ESM package |
force | Re-bundle, ignoring the cache |
entries | Custom entry points for the crawl |
The Cache
| Aspect | Value |
|---|---|
| Location | node_modules/.vite/deps |
| Invalidation | Lockfile, patches, config, NODE_ENV |
| Browser cache | max-age=31536000,immutable |
| Force re-bundle | --force flag |
Best Practices
✅ Do This:
# Let the automatic discovery handle the common dependencies
npm run dev # ✅
# Use the include option for the runtime-discovered dependencies
optimizeDeps: { include: ['echarts/core'] } # ✅
# Use the exclude option for the small ESM packages
optimizeDeps: { exclude: ['@my/utils'] } # ✅
# Use the force option when the cache is stale
npm run dev -- --force # ✅
❌ Don’t Do This:
# Exclude a CommonJS dependency
optimizeDeps: { exclude: ['some-cjs-lib'] } // Breaks # ❌
# Expect the source code to be pre-bundled
# Only the dependencies are pre-bundled # ❌
# Ignore the cache invalidation
# The stale cache causes the runtime errors # ❌
# Ship the pre-bundled dev output to production
# The production build is separate # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The CommonJS dependency fails | Excluded from the pre-bundling | Remove it from exclude |
| The runtime dependency is not found | The crawl missed it | Add it to include |
| The local package change is not reflected | The cache is stale | Use --force |
| The browser floods the server | A large ESM dependency is not pre-bundled | Add it to include |
Real-World Examples
1. The automatic pre-bundling
npm run dev
2. The include option
optimizeDeps: { include: ['echarts/core'] }
3. The exclude option
optimizeDeps: { exclude: ['@my/utils'] }
4. The force option
optimizeDeps: { force: true }
5. The cache directory
ls node_modules/.vite/deps/
6. The force flag
npm run dev -- --force
7. The cache invalidation
rm -rf node_modules/.vite
8. The pre-bundled React
ls node_modules/.vite/deps/react.js
9. The CommonJS conversion
// The CommonJS dependency becomes ESM
import { useState } from 'react'
10. The browser cache header
curl -I http://localhost:5173/node_modules/.vite/deps/react.js
# Cache-Control: max-age=31536000,immutable
Visual
The Pre-Bundling vs the On-Demand
┌──────────────────────────────────────────────────────────────┐
│ THE SOURCE IS ON-DEMAND; THE DEPENDENCIES ARE PRE-BUNDLED │
│ │
│ Source code: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ The browser requests the module │ │
│ │ → Vite transforms it on demand │ │
│ │ → The browser requests the next module │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Dependencies: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ The dependencies are pre-bundled at startup │ │
│ │ → The browser requests one merged file │ │
│ │ → The internal modules are hidden │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Request Flood
┌──────────────────────────────────────────────────────────────┐
│ lodash-es HAS OVER 600 INTERNAL MODULES │
│ │
│ Without the pre-bundling: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ import { debounce } from 'lodash-es' │ │
│ │ → the browser fires 600+ HTTP requests │ │
│ │ → network congestion, slow load │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ With the pre-bundling: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ import { debounce } from '/node_modules/.vite/deps/ │ │
│ │ lodash-es.js' │ │
│ │ → the browser fires 1 HTTP request │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Cache
┌──────────────────────────────────────────────────────────────┐
│ THE CACHE IS INVALIDATED BY THE LOCKFILE CHANGE │
│ │
│ node_modules/.vite/deps/ │
│ ├── react.js?v=abc123 │
│ └── lodash-es.js?v=def456 │
│ │
│ When the lockfile changes: │
│ → the hash changes │
│ → the browser requests the new file │
│ → the old cache is ignored │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| What is pre-bundled | The dependencies from node_modules |
| What is not pre-bundled | The source code |
| Why CommonJS | The browser cannot execute it |
| Why the flood | The ESM dependencies with many internal modules |
| Tool (Vite 7) | esbuild |
| Tool (Vite 8) | Rolldown |
| Cache location | node_modules/.vite/deps |
| Cache invalidation | Lockfile, patches, config, NODE_ENV |
| Force re-bundle | --force flag |
| Production build | Separate bundler |
Key takeaways:
- The dependencies are pre-bundled for the CommonJS compatibility. The browser cannot execute the CommonJS or UMD dependencies, so Vite converts them to ESM before the browser sees them .
- The dependencies are pre-bundled for the performance. A library like
lodash-eshas over 600 internal modules, and serving them unbundled would cause the browser to fire 600+ HTTP requests. The pre-bundling merges them into one file . - The pre-bundling applies only in development. The production build uses a different bundler for the optimized output. The pre-bundling is a dev-server optimization, not a production feature .
- The automatic discovery crawls the source code. Vite finds the bare imports and uses them as the entry points for the pre-bundle. The
optimizeDeps.includeoption forces the pre-bundling of a dependency the crawl missed . - The cache is stored in
node_modules/.vite/deps. The cache is invalidated when the lockfile, the patches, the config, or theNODE_ENVchanges. The--forceflag is the manual override . - The CommonJS dependencies should not be excluded. The
excludeoption is for the small ESM packages that are already valid ESM. Excluding a CommonJS dependency breaks the browser execution .
Remember: The pre-bundling is the exception to the on-demand rule. The source code is served on demand, but the dependencies are pre-bundled once. The pre-bundling solves the CommonJS compatibility and the request flood, and the cache keeps the startup fast. The optimizeDeps configuration controls the automatic discovery, and the --force flag is the manual override. The result is the dev server that starts in milliseconds and serves the dependencies without the browser drowning in the requests.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!