Mastering Vite’s Dependency Pre‑Bundling in Monorepos: When to Include, Exclude, or Leave It Alone
Vite’s dependency pre‑bundling can make or break a monorepo dev workflow. Learn when to include or exclude workspace packages, how to fine‑tune <code>optimizeDeps</code>, and avoid common pitfalls with a concrete example and practical checklist.
05 Sept 2026, 20:32 UTC

Why the Opt‑Deps Decision Matters
When you spin up a Vite dev server, it first runs esbuild to pre‑bundle every dependency it finds. In a monorepo, that process becomes more complex: some packages are local workspaces, others live in node_modules. The optimizeDeps config tells Vite how to treat those workspace packages. Choosing the wrong setting can make the dev server sluggish, break hot‑module replacement (HMR), or even cause runtime errors.
What Vite Does by Default
Vite scans the entry point (default index.html) for imports. For each import it:
- Converts CommonJS/UMD modules to ESM.
- Flattens nested
node_modulestrees. - Writes the result to
node_modules/.vite/depsand creates a_metadata.jsonrecord.
Workspace packages are treated as external unless you explicitly tell Vite otherwise. That means the dev server loads the package’s dist output (or its package.json exports) instead of bundling the source.
When to include a Workspace Package
Use case: The package is under active development and you want HMR to propagate changes instantly.
// vite.config.ts
export default defineConfig({
optimizeDeps: {
include: ['ui-lib'] // workspace package name
}
});
What happens:
- Vite bundles
ui-lib/srcinto the pre‑bundle. - Any change to
ui-libtriggers a full pre‑bundle rebuild, which can be expensive. - Because the source is bundled, it must be valid ESM for the browser. Node built‑ins (fs, path) will break unless shimmed.
Typical workflow: pnpm install, vite in the consumer app. If you modify ui-lib, the dev server will rebuild the pre‑bundle and hot‑reload the app.
When to exclude a Workspace Package
Use case: The package already ships a built dist folder that contains all necessary transformations (tree‑shaking, transpilation, etc.).
// vite.config.ts
export default defineConfig({
optimizeDeps: {
exclude: ['ui-lib']
}
});
What happens:
- Vite skips bundling the source.
- It loads the built
distentry (respectingpackage.jsonexports). - Cold start is slower because Vite must read the built files, but subsequent HMR is fast.
- You must run
pnpm run build(or similar) onui-libbefore starting the consumer dev server.
Risk: If the package’s dist is stale, the consumer app will run outdated code. A common mitigation is to add a pre‑start script that builds the workspace:
// package.json of consumer app
"scripts": {
"dev": "pnpm --filter ui-lib run build && vite"
}
Fine‑Grained Control with entries
In large monorepos you often have multiple apps, each with its own index.html. Vite’s optimizeDeps.entries lets you tell it to scan all those entry points in one go, preventing duplicate pre‑bundles:
// vite.config.ts
export default defineConfig({
optimizeDeps: {
entries: ['apps/*/index.html']
}
});
Result: Each shared dependency is pre‑bundled once, saving memory and startup time.
Concrete Example: A UI Library & a Consumer App
- Create a workspace root with
pnpm-workspace.yaml:packages: - "packages/*" - Define
packages/ui-libwith a simple component that importslodash. - Define
packages/appthat importsui-liband renders it. - In
packages/app/vite.config.ts, setoptimizeDeps.include: ['ui-lib']for rapid HMR. - Run
pnpm installandpnpm --filter app run dev. - Modify a file in
ui-lib/srcand observe the dev server rebuilding the pre‑bundle and hot‑reloading the app. - Now change the config to
exclude: ['ui-lib']and add a pre‑build step. - Run
pnpm --filter ui-lib run buildthenpnpm --filter app run dev. - Notice the longer cold start but instant HMR when you touch
ui-lib/src(since the builtdistis already up‑to‑date).
Use vite --debug to inspect the [vite] deps logs and confirm whether ui-lib is bundled or externalized.
Trade‑Offs & Limitations
- Memory & Startup: Including many workspace packages inflates the pre‑bundle cache. Excluding them keeps the cache lean but forces a build step.
- Build Consistency: Excluding relies on a separate build pipeline. If that pipeline fails or is skipped, the consumer app will break.
- ESM Compatibility: Bundling source requires the package to be pure ESM. Node‑only APIs or build‑time macros need shims or must be excluded.
- Dynamic Imports: Vite’s initial scan ignores
import()calls. Those deps trigger a secondary pre‑bundle pass, causing a brief pause.
Practical Checklist Before You Commit
- Does the workspace package change often? Include it.
- Is the package already built and ESM‑ready? Exclude it and add a pre‑build step.
- Does the package use Node built‑ins? Either shim them or exclude.
- How many shared deps exist? Use
entriesto avoid duplication. - After changing
optimizeDeps, runvite --forceto clear the cache.
Actionable Takeaway
In a monorepo, treat optimizeDeps as a tuning knob rather than a one‑size‑fits‑all setting. Start with the default, then:
- Include only the packages you actively edit.
- Exclude the rest and ensure they’re built before the consumer starts.
- Use
entriesto scan all app entry points. - Regularly run
vite --debugto keep an eye on what’s being bundled.
With these practices, you’ll keep dev‑server start times low, HMR fast, and build outputs reliable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.