Why Your Vite Dev Server Starts Fast: Dependency Pre-Bundling with optimizeDeps
Vite pre-bundles your dependencies with esbuild at dev-server startup, turning hundreds of module requests into one per package. Here's how it works, how to verify it, and when to tune optimizeDeps.
12 Mar 2026, 04:09 UTC

You add lodash-es to a Vite project, hit npm run dev, and the page loads in a blink — even though lodash-es ships as hundreds of tiny ES modules. The reason isn't magic: before your browser ever makes a request, Vite has already run esbuild over your dependencies and collapsed each package into a single file. That step is called dependency pre-bundling, and understanding it saves real debugging time when a dependency behaves oddly in dev but fine in production.
The problem pre-bundling solves
Vite's dev server serves your source code as native ES modules, transforming files on demand. That works beautifully for your own code, but raw node_modules packages are a poor fit for two reasons:
- Request waterfalls. A package like
lodash-eshas 600+ internal modules. Serving them individually means hundreds of HTTP requests before your app renders, even over HTTP/2 on localhost. - CommonJS interop. Many published packages are still CommonJS or UMD. Browsers can't
importthose natively, so they must be converted to ESM first.
Pre-bundling fixes both: esbuild converts each dependency to ESM and bundles it into one module, so the browser makes a single request per package instead of hundreds.
What actually happens at startup
When you run vite, the server scans your entry points (starting from index.html), follows imports, and identifies bare imports — things like import _ from 'lodash' rather than relative paths. Each discovered dependency is passed to esbuild, which transpiles and bundles it into a single ESM file cached under node_modules/.vite/deps.
From then on, imports of that package are rewritten to point at the cached file. The browser loads it directly with no further transformation, which is why hot module replacement feels instant: Vite only transforms your code on change, not the entire dependency graph.
The cache is invalidated automatically when your lockfile changes (package-lock.json, yarn.lock, or pnpm-lock.yaml), so installing or upgrading a package triggers a re-bundle on the next server start. You can also force it manually by deleting node_modules/.vite or starting with vite --force.
A worked example: verifying it yourself
This is easy to confirm in any Vite project. Run the dev server from your project root (no special permissions needed):
npm run devThen open your browser's devtools, go to the Network tab, and filter for .js. If your app imports lodash-es, you should see a single request to something like:
/node_modules/.vite/deps/lodash-es.js?v=abc123The response should have Content-Type: application/javascript and contain the whole package's bundled source. The ?v= hash is Vite's cache-busting token — it changes when the lockfile changes, which is how the browser knows to refetch.
To watch re-bundling happen: add a new dependency, run npm install, restart the dev server, and check the Network tab again. A new pre-bundled file for the added package appears without any manual step.
Tuning it with the optimizeDeps config
The defaults cover most projects, but optimizeDeps in vite.config.js gives you control when auto-discovery falls short:
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
optimizeDeps: {
// Force-bundle a package Vite's scanner missed
// (e.g., one imported only dynamically)
include: ['some-dynamic-dep'],
// Keep a package out of pre-bundling — useful for
// linked local packages you're actively editing
exclude: ['my-linked-lib'],
},
})Two common use cases:
includefor dependencies only reached through dynamic imports or plugin-generated code, which the static scanner can't see.excludefor a local package linked vianpm link, so your edits to it are picked up through normal HMR instead of being frozen into a cached bundle.
Note that changing optimizeDeps requires a dev server restart — Vite keeps serving the previously cached bundles otherwise.
The trade-off: dev/prod divergence
Pre-bundling is a dev-only optimization (your production build goes through Rollup instead), and that gap occasionally bites. The classic failure mode: a dependency relies on side effects in its entry file that esbuild's tree-shaking drops, or a package with tricky CommonJS interop behaves slightly differently after conversion. The symptom is always the same — "works in build, broken in dev" or vice versa.
When you hit that, the diagnostic path is short:
- Restart with
vite --forceto rule out a stale cache. - Add the package to
optimizeDeps.include(orexcludeit) and restart, to see whether pre-bundling is the variable. - If excluding it fixes dev, check whether the package ships a proper ESM build — that's often the root cause worth reporting upstream.
Takeaway
Pre-bundling is why Vite's dev server stays fast as your dependency count grows: one esbuild pass at startup turns messy node_modules into clean, single-file ESM the browser can load directly. You rarely need to touch it — but when a dependency misbehaves in dev, node_modules/.vite, the ?v= hash, and the optimizeDeps.include/exclude knobs are the first places to look.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.