Vite Patterns
Quick Guide: Vite serves unbundled ES modules in development and bundles for production, so most configuration decisions apply to only one of those halves. The version decides the API: Vite 8 bundles with Rolldown (
,
,
), Vite 7 with Rollup (
,
, aliases mirrored into
by hand). Sass runs on the modern API from Vite 6 onwards, and the legacy API is gone from Vite 7.
Detailed Resources:
- examples/core.md — full configs for aliases, chunk splitting, environment-specific builds, module preload, Sass, the Environment API and the dev proxy
- reference.md — dev/production settings, build-target table, per-version notes and the Vite 8 migration checklist
Which path applies
Read the
version in
before writing config; the two branches share names and not behaviour.
- Vite 8 (Rolldown) — , for chunking, instead of hand-mirrored aliases. still resolves as a deprecated alias, which is why a wrong-version config looks like it works.
- Vite 7 (Rollup) — with , and aliases declared in both and .
<critical_requirements>
Before writing Vite config
Declare path aliases in one place per version —
on Vite 8, or the same set in both
and
on Vite 7. A pair that has drifted resolves in one tool and not the other, so the build and the editor disagree about the same import.
Match the bundler options to the version. On Vite 8 that is
with
; object-form
is removed there and silently produces no grouping.
Split config by mode rather than shipping one build. defineConfig(({ mode }) => …)
with
keeps minification and sourcemaps out of development and out of the production bundle respectively.
Reach for build.modulePreload.polyfill
, the current spelling —
build.polyfillModulePreload
is deprecated and offers no control over which dependencies are preloaded.
</critical_requirements>
Auto-detection: Vite, vite.config.ts, vite.config.js, defineConfig, loadEnv, manualChunks, advancedChunks, codeSplitting.groups, rolldownOptions, rollupOptions, modulePreload, resolve.alias, resolve.tsconfigPaths, build.target, baseline-widely-available, VITE_
Applies to:
- Configuring a Vite build for a frontend application
- Path aliases, and keeping them resolvable by both the bundler and the type checker
- Vendor chunk splitting for cacheable production output
- Per-mode builds (development, staging, production) with and
- Migrating a config from Vite 7 to Vite 8
- Module preload, build targets and Sass preprocessing
Handled elsewhere:
- Compiler options and type-checking behaviour, beyond the entry an alias needs
- Lint and format configuration
- Application code that runs in the browser — this is build-time configuration only
- Build orchestration by an SSR meta-framework, which owns its own config file
- Pipeline and deployment concerns around the build command
<philosophy>
Development and production run different machinery. The dev server transforms modules on demand and ships them unbundled, so nothing there is tree-shaken, minified or chunked; the production build is a bundler run with all three. Every defect that survives to release lives in that gap — a stylesheet dropped by tree-shaking, a chunk that only exists in production, an env var that was present locally.
So the useful question about any option is which half it governs, and whether the other half will ever exercise it.
</philosophy>
<decision_framework>
Chunk splitting
Vite 8 (Rolldown)?
├─ Separating vendors by package → codeSplitting.groups with a test regex ✓
└─ Constraining by size or share count → codeSplitting maxSize / minSize / minShareCount
Vite 7 (Rollup)?
├─ Separating vendors by package → manualChunks, object form ✓
└─ Logic per module id → manualChunks, function form
is the same feature under its rolldown-vite name; it became
in Vite 8.
Build target
Are there browsers below the default floor to support?
├─ YES → @vitejs/plugin-legacy alongside the modern build
└─ NO → Smallest possible output?
├─ YES → 'esnext'
└─ NO → 'baseline-widely-available' (the default) ✓, or an explicit list
such as ['chrome111', 'safari16.4'] when a contract names versions
</decision_framework>
<patterns>
Core patterns
Pattern 1: Path Aliases
One alias set, resolvable by the bundler and the type checker alike.
typescript
// Vite 8 — read straight from tsconfig.json
resolve: { tsconfigPaths: true },
// Vite 7 — declared here and mirrored in tsconfig.json "paths"
resolve: {
alias: { "@": path.resolve(__dirname, "./src") },
},
is off by default because resolving through tsconfig costs startup time; turn it on only where the project uses
. It covers only what
declares, so an alias with no
entry — a package outside the project, a stub swapped in for a build — still needs
beside it.
Full code: examples/core.md
Pattern 2: Vendor Chunk Splitting
Dependencies change on a slower cadence than application code, so a separate chunk stays in the browser cache across deploys.
typescript
// Vite 8
build: {
rolldownOptions: {
output: {
codeSplitting: {
groups: [{ name: "vendor", test: /[\\/]node_modules[\\/]/ }],
},
},
},
},
Narrow the
per group where different dependencies move at different rates; one chunk holding everything is invalidated by any upgrade.
Full code, including the Vite 7 form: examples/core.md
Pattern 3: Per-Mode Builds
takes a function, so the config can differ per
.
typescript
export default defineConfig(({ mode }) => ({
build: {
sourcemap: mode !== "production",
minify: mode === "production",
},
}));
file selection follows
, not
.
returns every variable, including ones with no client prefix, so treat its result as server-side values rather than something to feed into
.
Full code: examples/core.md
Pattern 4: Module Preload
typescript
build: {
modulePreload: {
polyfill: false,
resolveDependencies: (filename, deps) =>
deps.filter((dep) => !dep.includes("rarely-used-vendor")),
},
},
Dropping the polyfill is safe only where the target floor supports module preload natively;
is how a large chunk is kept out of the initial preload list.
Full code: examples/core.md
Pattern 5: Sass
typescript
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "./src/styles/variables" as *;`,
},
},
},
The modern API is the default from Vite 6 and the only one from Vite 7, so a config still passing
fails rather than falling back. Migrating means
becoming
/
with namespaced access.
Pattern 6: Dev Server Proxy
typescript
server: {
proxy: {
"/api": { target: "http://localhost:8000", changeOrigin: true },
},
},
Proxying keeps development same-origin, so no CORS configuration is needed for a backend that will be same-origin in production anyway.
Pattern 7: Environment API
For framework authors and multi-runtime builds; still RC as of Vite 8, and an ordinary single-client app needs none of it.
typescript
environments: {
client: { build: { outDir: "dist/client" } },
ssr: { build: { outDir: "dist/server", ssr: true } },
},
Full code: examples/core.md
</patterns>
<red_flags>
Red flags
Breaks at runtime:
- Object-form on Vite 8 — removed, so the grouping silently does not happen and the vendor chunk you expected is not in
- , removed in Vite 7 — the import throws before the build starts
- , removed in Vite 7 — replaced by
'baseline-widely-available'
- Sass on Vite 7 or later — the legacy API is gone rather than deprecated
- An alias present in and missing from , or the reverse — the build succeeds and the type check fails, or the editor resolves an import the bundler cannot
Surprising behaviour:
- still resolves on Vite 8 as a deprecated alias, so a Vite 7 config appears to work until a Rolldown-only option is needed
- is off by default; enabling it is a deliberate step, not a fallback
- Rolldown emits an extra chunk beside the code-split groups
- loads every variable in the file, prefix or not — inlining one of those through publishes it in the bundle
- is a no-op on Vite 8, since Rolldown handles CommonJS natively
- The option auto-converts to on Vite 8, quietly dropping the options Oxc has no equivalent for
- Sourcemaps left on in production ship readable source; keeps them for error reporting without linking them from the bundle
</red_flags>