Vite Advanced
Advanced Vite 7+ patterns including Environment API, plugin development, SSR configuration, library mode, and build optimization. Use when customizing build pipelines, creating plugins, or configuring multi-environment builds.
Primary Agent: frontend-ui-developer
Vite Advanced Patterns
Advanced configuration for Vite 7+ including Environment API.
Vite 7 Environment API (Key 2026 Feature)
Multi-environment support is now first-class:
import { defineConfig } from 'vite'
export default defineConfig({
environments: {
// Browser client
client: {
build: {
outDir: 'dist/client',
manifest: true,
},
},
// Node.js SSR
ssr: {
build: {
outDir: 'dist/server',
target: 'node20',
},
},
// Edge runtime (Cloudflare, etc.)
edge: {
resolve: {
noExternal: true,
conditions: ['edge', 'worker'],
},
build: {
outDir: 'dist/edge',
},
},
},
})Key Changes:
- Environments have their own module graph
- Plugins access
this.environmentin hooks createBuilderAPI for coordinated builds- Node.js 20.19+ or 22.12+ required
Plugin Development
Basic plugin structure:
export function myPlugin(): Plugin {
return {
name: 'my-plugin',
// Called once when config is resolved
configResolved(config) {
// Access resolved config
},
// Transform individual modules
transform(code, id) {
// this.environment available in Vite 7+
if (id.endsWith('.special')) {
return { code: transformCode(code) }
}
},
// Virtual modules
resolveId(id) {
if (id === 'virtual:my-module') {
return '\0virtual:my-module'
}
},
load(id) {
if (id === '\0virtual:my-module') {
return 'export const value = "generated"'
}
},
}
}SSR Configuration
Development (middleware mode):
import { createServer } from 'vite'
const vite = await createServer({
server: { middlewareMode: true },
appType: 'custom',
})
app.use('*', async (req, res) => {
const url = req.originalUrl
let template = fs.readFileSync('index.html', 'utf-8')
template = await vite.transformIndexHtml(url, template)
const { render } = await vite.ssrLoadModule('/src/entry-server.tsx')
const html = template.replace('<!--outlet-->', await render(url))
res.send(html)
})Production build scripts:
{
"scripts": {
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outDir dist/server --ssr src/entry-server.tsx"
}
}Build Optimization
export default defineConfig({
build: {
target: 'baseline-widely-available', // Vite 7 default
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
},
},
},
},
})Quick Reference
| Feature | Vite 7 Status |
|---|---|
| Environment API | Stable |
| ESM-only distribution | Default |
| Node.js requirement | 20.19+ or 22.12+ |
buildApp hook | New for plugins |
createBuilder | Multi-env builds |
Vite 8: Rolldown-Powered Builds
Vite 8 replaces the esbuild+Rollup pipeline with Rolldown, a Rust-based unified bundler delivering dramatic performance improvements.
Migration Options
Option 1: Direct Upgrade to Vite 8
npm install vite@8Best for: New projects, smaller codebases, teams ready to adopt cutting-edge tooling.
Option 2: Gradual Migration with rolldown-vite
npm install rolldown-vite// vite.config.ts - swap import only
import { defineConfig } from 'rolldown-vite' // instead of 'vite'
export default defineConfig({
// Existing config works unchanged
})Best for: Large production apps, risk-averse teams, testing Rolldown before full commitment.
Performance Benchmarks
Real-world improvements from production deployments:
| Metric | Before (Vite 7) | After (Vite 8) | Improvement |
|---|---|---|---|
| Linear build time | 46s | 6s | 7.7x faster |
| Dev server startup | ~3s | ~1s | 3x faster |
| HMR updates | ~100ms | ~60ms | 40% faster |
| Memory usage | ~800MB | ~400MB | 50% reduction |
advancedChunks (Replaces manualChunks)
Vite 8 introduces advancedChunks with declarative grouping, priority control, and size constraints:
export default defineConfig({
build: {
rollupOptions: {
output: {
// NEW: advancedChunks replaces manualChunks
advancedChunks: {
groups: [
{
name: 'react-vendor',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
priority: 20,
minSize: 20000, // 20KB minimum
maxSize: 250000, // 250KB maximum
},
{
name: 'ui-vendor',
test: /[\\/]node_modules[\\/]@radix-ui[\\/]/,
priority: 15,
minShareCount: 2, // Must be used by 2+ chunks
},
{
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 10,
maxSize: 500000, // Auto-split if exceeds 500KB
},
],
},
},
},
},
})Key Differences from manualChunks:
| Feature | manualChunks | advancedChunks |
|---|---|---|
| Syntax | Function or object | Declarative groups array |
| Priority control | Manual ordering | Explicit priority field |
| Size constraints | None | minSize, maxSize |
| Shared module handling | Manual | minShareCount |
| Regex support | Via function | Native test field |
When to Use Vite 7 vs Vite 8
| Scenario | Recommendation | Reason |
|---|---|---|
| New greenfield project | Vite 8 | Latest features, best performance |
| Existing stable production app | Vite 7 (evaluate 8) | Stability, proven track record |
| Build times > 30s | Vite 8 | Significant improvement |
| Complex plugin ecosystem | Vite 7 (test 8) | Some plugins may need updates |
| Monorepo with many packages | Vite 8 | Memory and speed benefits |
| Enterprise with strict stability | Vite 7 | LTS-style support |
Full Bundle Mode (Upcoming)
Vite 8.1+ will introduce optional Full Bundle Mode for production builds:
export default defineConfig({
build: {
// Preview API - may change
fullBundleMode: true,
},
})Benefits:
- Single unified bundle (no code splitting)
- Optimal for small apps, libraries, or embedded contexts
- Eliminates chunk loading overhead
- Better for offline-first applications
Oxc Integration Benefits
Rolldown is built on Oxc (Oxidation Compiler), providing:
- Parsing: 3x faster than SWC, 100x faster than Babel
- Transformation: Unified transform pipeline
- Tree-shaking: More aggressive dead code elimination
- Scope hoisting: Better than Rollup's implementation
- Minification: Oxc minifier (optional, in development)
export default defineConfig({
build: {
// Future Oxc minifier option (when stable)
// minify: 'oxc',
},
})Migration Checklist
[ ] Review plugin compatibility (most work unchanged)
[ ] Test with rolldown-vite first if risk-averse
[ ] Replace manualChunks with advancedChunks
[ ] Remove esbuild-specific workarounds (no longer needed)
[ ] Update CI/CD build time expectations
[ ] Test HMR behavior (should be faster, same API)
[ ] Verify source maps work correctlyStatus: Vite 8 stable as of Feb 2026. Recommended for new projects; evaluate for existing production apps.
Key Decisions
| Decision | Recommendation |
|---|---|
| Multi-env builds | Use Vite 7 Environment API |
| Plugin scope | Use this.environment for env-aware plugins |
| SSR | Middleware mode for dev, separate builds for prod |
| Chunks | Manual chunks for vendor/router separation |
Related Skills
biome-linting- Fast linting alongside Viteork:react-server-components-framework- SSR integrationedge-computing-patterns- Edge environment builds
References
- Environment API - Multi-environment builds
- Plugin Development - Plugin hooks
- SSR Configuration - SSR setup
- Library Mode - Building packages
- Chunk Optimization - Build optimization
Rules (4)
Split Vite chunks for granular caching and faster initial loads instead of single-bundle shipping — HIGH
Vite: Chunk Optimization
Use advancedChunks (Vite 8+) or manualChunks (Vite 7) to split vendor and application code into separate, cacheable chunks. Assign priorities to resolve conflicts and use maxSize to prevent oversized bundles.
Incorrect:
// No chunk config — everything in a single monolithic bundle
export default defineConfig({
build: { rollupOptions: {} }, // No advancedChunks or manualChunks
})Correct (Vite 8+ — advancedChunks):
export default defineConfig({
build: {
rollupOptions: {
output: {
advancedChunks: {
groups: [
{
name: 'react-vendor',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
priority: 30,
minSize: 20000,
maxSize: 200000,
},
{
name: 'router',
test: /[\\/]node_modules[\\/](react-router|react-router-dom)[\\/]/,
priority: 25,
},
{
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 5,
maxSize: 500000, // Auto-splits into vendor, vendor-1, etc.
},
],
},
},
},
},
})Correct (Vite 7 — manualChunks):
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'router': ['react-router-dom'],
},
},
},
},
})Key rules:
- Separate framework deps into a dedicated vendor chunk with the highest priority so it caches independently from app code.
- Add a catch-all
vendorgroup at lowest priority withmaxSizeto prevent oversized bundles. - Use
minShareCountfor shared UI libraries — only extract when imported by 2+ routes. - When migrating Vite 7 to 8, convert package arrays to regex and make implicit ordering explicit via
priority. manualChunksis deprecated in Vite 8 — preferadvancedChunksfor new projects.
Reference: references/chunk-optimization.md
Configure Vite environment API to separate client and SSR build targets correctly — MEDIUM
Vite: Environment API
Vite 6+ treats environments (client, SSR, edge) as first-class concepts, each with its own module graph, config, plugin pipeline, and build output. Use environments config instead of mixing targets in top-level config.
Incorrect:
// Flat config — SSR and client share the same target and externals
export default defineConfig({
build: {
outDir: 'dist',
target: 'node20', // Wrong for client!
rollupOptions: { external: ['cloudflare:workers'] }, // Wrong for client!
},
ssr: { noExternal: ['some-package'] }, // Legacy SSR config
})Correct:
export default defineConfig({
build: { sourcemap: false }, // Shared config
environments: {
client: {
build: { outDir: 'dist/client', manifest: true },
},
ssr: {
build: {
outDir: 'dist/server',
target: 'node20',
rollupOptions: { output: { format: 'esm' } },
},
},
edge: {
resolve: { noExternal: true, conditions: ['edge', 'worker'] },
build: {
outDir: 'dist/edge',
rollupOptions: { external: ['cloudflare:workers'] },
},
},
},
})Correct — environment-aware plugins:
export function envAwarePlugin(): Plugin {
return {
name: 'env-aware',
transform(code, id) {
const env = this.environment // Available in Vite 6+
if (env.name === 'ssr') return transformForSSR(code)
if (env.name === 'edge') return transformForEdge(code)
return transformForClient(code)
},
}
}Key rules:
- Put only shared settings at the top level; environment-specific settings go under
environments.client,environments.ssr, etc. - Each environment gets its own module graph and build output — never share
outDirbetween environments. - Use
this.environmentin hooks to branch per environment; useperEnvironmentPlugin()to skip environments entirely. - For edge runtimes, set
resolve.noExternal: trueandresolve.conditionsfor edge-specific package exports. - Vite 7 requires Node.js 20.19+ or 22.12+ for
require(esm)support.
Reference: references/environment-api.md
Configure Vite library mode with correct externals, exports, and type declarations — HIGH
Vite: Library Mode
Configure build.lib with proper entry points, externalize peer dependencies, and provide dual ESM/CJS output with TypeScript declarations.
Incorrect:
// Bundles React into the library — consumers get duplicate React
export default defineConfig({
build: {
lib: { entry: resolve(__dirname, 'src/index.ts'), formats: ['es'] },
// Missing rollupOptions.external — peer deps are bundled
},
})Correct:
import { defineConfig } from 'vite'
import { resolve } from 'path'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [dts({ include: ['src'], rollupTypes: true })],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
fileName: (format) => `my-lib.${format}.js`,
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: { react: 'React', 'react-dom': 'ReactDOM' },
},
},
},
}){
"name": "my-lib",
"type": "module",
"main": "./dist/my-lib.umd.js",
"module": "./dist/my-lib.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/my-lib.es.js",
"require": "./dist/my-lib.umd.js",
"types": "./dist/index.d.ts"
},
"./styles.css": "./dist/style.css"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"sideEffects": ["**/*.css"]
}Key rules:
- Always externalize peer dependencies via
rollupOptions.external— never bundle them. - Provide dual formats: ESM (
module) for bundlers and UMD/CJS (main) for legacy consumers; useexportsmap. - Generate TypeScript declarations with
vite-plugin-dts; set"types"in top-level and eachexportsentry. - Mark CSS in
"sideEffects"so bundlers preserve styles during tree-shaking. - For multi-entry libraries, use an object
entryand match keys toexportssubpaths.
Reference: references/library-mode.md
Use correct Vite plugin hooks with enforce and apply modifiers to avoid silent failures — MEDIUM
Vite: Plugin Hooks
Vite plugins follow a strict hook execution order inherited from Rollup. Choose the correct hook for each task and use enforce/apply to control when a plugin runs.
Hook execution order:
1. config — Modify config before resolution
2. configResolved — Access final config (read-only)
3. configureServer — Dev server setup (dev only)
4. buildStart — Build begins
5. resolveId — Resolve import paths to module IDs
6. load — Provide module content for a resolved ID
7. transform — Transform loaded module source code
8. buildEnd / closeBundle — CleanupIncorrect:
// Wrong: transform can't create modules — virtual modules need resolveId + load
export function brokenVirtualPlugin(): Plugin {
return {
name: 'broken-virtual',
transform(code, id) {
if (id === 'virtual:my-data') {
return `export default ${JSON.stringify({ key: 'value' })}`
}
},
}
}Correct:
const VIRTUAL_ID = 'virtual:my-data'
const RESOLVED_ID = '\0' + VIRTUAL_ID
export function virtualDataPlugin(data: Record<string, unknown>): Plugin {
return {
name: 'virtual-data',
resolveId(id) {
if (id === VIRTUAL_ID) return RESOLVED_ID
},
load(id) {
if (id === RESOLVED_ID) return `export default ${JSON.stringify(data)}`
},
}
}Correct — enforce and apply modifiers:
export function preProcessPlugin(): Plugin {
return {
name: 'pre-process',
enforce: 'pre', // Run BEFORE core Vite plugins
apply: 'build', // Only during vite build (not dev)
transform(code, id) {
if (!id.endsWith('.special.ts')) return null
return { code: code.replace(/PLACEHOLDER/g, 'REPLACED'), map: null }
},
}
}Key rules:
- Use
resolveId+loadfor virtual modules;transformonly modifies already-loaded source. - Prefix resolved virtual IDs with
\0to exclude them from other plugins and filesystem resolution. - Set
enforce: 'pre'to run before core plugins,enforce: 'post'to run after. - Set
apply: 'build'orapply: 'serve'to restrict a plugin to one mode. - Access
this.environmentin hooks (Vite 6+) for environment-specific transforms.
Reference: references/plugin-development.md
References (5)
Chunk Optimization
Vite Build Optimization
Chunk splitting and build performance.
advancedChunks (Vite 8+)
Vite 8 introduces advancedChunks as the recommended approach for chunk splitting. It provides declarative configuration with priority control and size constraints.
Full Syntax
export default defineConfig({
build: {
rollupOptions: {
output: {
advancedChunks: {
groups: [
{
// Required: Chunk name
name: 'react-vendor',
// Required: Module matching (regex or function)
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
// Optional: Higher priority wins when module matches multiple groups
priority: 20,
// Optional: Minimum chunk size in bytes (default: 0)
minSize: 20000, // 20KB
// Optional: Maximum chunk size in bytes (auto-splits if exceeded)
maxSize: 250000, // 250KB
// Optional: Minimum number of chunks that must share this module
minShareCount: 1,
},
],
},
},
},
},
})Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
name | string | Required | Output chunk name |
test | RegExp | (id: string) => boolean | Required | Module matcher |
priority | number | 0 | Higher wins on conflicts |
minSize | number | 0 | Skip if chunk would be smaller |
maxSize | number | Infinity | Auto-split if exceeded |
minShareCount | number | 1 | Required shared imports |
Complete Example
// vite.config.ts - Production-ready advancedChunks
export default defineConfig({
build: {
rollupOptions: {
output: {
advancedChunks: {
groups: [
// Framework core - highest priority, always separate
{
name: 'react-vendor',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
priority: 30,
minSize: 20000,
maxSize: 200000,
},
// Router - medium-high priority
{
name: 'router',
test: /[\\/]node_modules[\\/](react-router|react-router-dom|@remix-run)[\\/]/,
priority: 25,
},
// UI library - group all Radix components
{
name: 'radix-ui',
test: /[\\/]node_modules[\\/]@radix-ui[\\/]/,
priority: 20,
minShareCount: 2, // Only if used by 2+ routes
},
// Charts - large, load on demand
{
name: 'charts',
test: /[\\/]node_modules[\\/](recharts|d3|victory)[\\/]/,
priority: 15,
maxSize: 300000, // Split if charts exceed 300KB
},
// Date libraries
{
name: 'dates',
test: /[\\/]node_modules[\\/](date-fns|dayjs|luxon)[\\/]/,
priority: 15,
},
// Catch-all vendor chunk
{
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 5,
maxSize: 500000, // Auto-split large vendor chunks
},
],
},
},
},
},
})Function-Based Test
For complex matching logic:
advancedChunks: {
groups: [
{
name: 'heavy-deps',
test: (id) => {
if (!id.includes('node_modules')) return false
const heavyPackages = ['three', 'monaco-editor', 'pdf-lib']
return heavyPackages.some(pkg => id.includes(`node_modules/${pkg}`))
},
priority: 25,
},
],
}Manual Chunks (Vite 7 and Earlier)
Deprecation Notice (Vite 8):
manualChunksstill works in Vite 8 for backward compatibility, butadvancedChunksis the recommended approach. Consider migrating toadvancedChunksfor new projects or when updating existing configurations.
Split large dependencies into separate chunks:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor chunk for React
'react-vendor': ['react', 'react-dom'],
// Router chunk
'router': ['react-router-dom'],
// UI library chunk
'ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
// Chart libraries
'charts': ['recharts', 'd3'],
},
},
},
},
})Dynamic Manual Chunks
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// All node_modules in vendor chunk
if (id.includes('node_modules')) {
// Split by package name
const match = id.match(/node_modules\/([^/]+)/)
if (match) {
const packageName = match[1]
// Group related packages
if (['react', 'react-dom', 'scheduler'].includes(packageName)) {
return 'react-vendor'
}
if (packageName.startsWith('@radix-ui')) {
return 'radix-vendor'
}
// Large packages get their own chunk
if (['lodash', 'moment', 'three'].includes(packageName)) {
return packageName
}
// Everything else in common vendor
return 'vendor'
}
}
},
},
},
},
})Build Target
Vite 7 default: 'baseline-widely-available'
export default defineConfig({
build: {
target: 'baseline-widely-available', // Default in Vite 7
// Or specific targets:
// target: 'esnext',
// target: 'es2022',
// target: ['es2022', 'edge88', 'firefox78', 'chrome87', 'safari14'],
},
})Minification
export default defineConfig({
build: {
minify: 'esbuild', // Default, fastest
// minify: 'terser', // More aggressive, slower
// Terser options
terserOptions: {
compress: {
drop_console: true, // Remove console.log
drop_debugger: true,
},
},
},
})Source Maps
export default defineConfig({
build: {
sourcemap: false, // No source maps (production)
// sourcemap: true, // Separate .map files
// sourcemap: 'inline', // Inline in JS (dev)
// sourcemap: 'hidden', // Maps for error reporting only
},
})Tree Shaking
Ensure packages support tree shaking:
// package.json of your lib
{
"sideEffects": false,
// Or specify files with side effects:
"sideEffects": ["**/*.css", "./src/polyfills.js"]
}Analyze Bundle
# Install visualizer
npm install -D rollup-plugin-visualizer
# Or use npx
npx vite-bundle-visualizerimport { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true,
filename: 'stats.html',
gzipSize: true,
brotliSize: true,
}),
],
})CSS Optimization
export default defineConfig({
build: {
cssCodeSplit: true, // Split CSS per entry point
cssMinify: 'lightningcss', // Faster CSS minification
},
css: {
devSourcemap: true, // CSS source maps in dev
},
})Asset Inlining
export default defineConfig({
build: {
assetsInlineLimit: 4096, // Inline assets < 4kb as base64
},
})Chunk Size Warnings
export default defineConfig({
build: {
chunkSizeWarningLimit: 500, // Warn if chunk > 500kb
},
})Dependency Optimization
export default defineConfig({
optimizeDeps: {
// Pre-bundle these dependencies
include: ['lodash-es', 'axios'],
// Don't pre-bundle these
exclude: ['@my/local-package'],
// Force re-optimization
force: true,
},
})Quick Optimization Checklist
- Use
advancedChunks(Vite 8+) ormanualChunks(Vite 7) - Use dynamic imports for routes
- Set appropriate
targetfor audience - Remove console in production
- Analyze bundle with visualizer
- Check for duplicate dependencies
- Ensure tree-shakeable imports
- Set
sideEffects: falsein package.json - Consider CSS code splitting
- Adjust
assetsInlineLimitas needed
Migration: manualChunks to advancedChunks
When upgrading to Vite 8, convert your manualChunks configuration to advancedChunks.
Example 1: Object-Based manualChunks
Before (Vite 7):
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'router': ['react-router-dom'],
'ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
}After (Vite 8):
advancedChunks: {
groups: [
{
name: 'react-vendor',
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
priority: 20,
},
{
name: 'router',
test: /[\\/]node_modules[\\/]react-router-dom[\\/]/,
priority: 15,
},
{
name: 'ui',
test: /[\\/]node_modules[\\/]@radix-ui[\\/]react-(dialog|dropdown-menu)[\\/]/,
priority: 10,
},
],
}Example 2: Function-Based manualChunks
Before (Vite 7):
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react') || id.includes('react-dom')) {
return 'react-vendor'
}
if (id.includes('@radix-ui')) {
return 'radix-vendor'
}
if (id.includes('lodash') || id.includes('moment')) {
return 'utils'
}
return 'vendor'
}
}After (Vite 8):
advancedChunks: {
groups: [
{
name: 'react-vendor',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
priority: 30,
},
{
name: 'radix-vendor',
test: /[\\/]node_modules[\\/]@radix-ui[\\/]/,
priority: 20,
},
{
name: 'utils',
test: /[\\/]node_modules[\\/](lodash|moment)[\\/]/,
priority: 15,
},
{
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 5,
},
],
}Example 3: Adding Size Constraints
One key benefit of advancedChunks is automatic chunk splitting based on size.
Before (Vite 7): No built-in size control
manualChunks: {
'vendor': ['lodash', 'moment', 'axios', 'd3', 'three'],
// This could produce a 2MB chunk with no warning
}After (Vite 8): Automatic splitting with maxSize
advancedChunks: {
groups: [
{
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 5,
maxSize: 250000, // Auto-split into vendor, vendor-1, vendor-2, etc.
},
],
}Example 4: Shared Module Optimization
Use minShareCount to only chunk modules used by multiple entry points.
After (Vite 8):
advancedChunks: {
groups: [
{
name: 'shared-ui',
test: /[\\/]node_modules[\\/]@radix-ui[\\/]/,
priority: 15,
minShareCount: 2, // Only if imported by 2+ chunks
},
{
name: 'shared-utils',
test: /[\\/]src[\\/]utils[\\/]/,
priority: 10,
minShareCount: 3, // Common utilities used across 3+ pages
minSize: 5000, // At least 5KB
},
],
}Migration Tips
- Start with existing chunk names - Keep the same
namevalues for cache consistency - Convert package arrays to regex -
['react', 'react-dom']becomes/[\\/]node_modules[\\/](react|react-dom)[\\/]/ - Add priorities - Earlier items in manualChunks had implicit priority; make it explicit
- Add size constraints - Use
maxSizeto prevent oversized chunks - Test thoroughly - Compare bundle output before/after with
rollup-plugin-visualizer
Compatibility Note
Both manualChunks and advancedChunks can coexist during migration, but advancedChunks takes precedence when both match the same module. For cleanest results, fully migrate to advancedChunks.
Environment Api
Vite 7 Environment API
Multi-environment builds for client, SSR, and edge runtimes.
Concept
Vite 6+ formalizes environments as a first-class concept. Each environment has its own:
- Module graph
- Configuration
- Plugin pipeline
- Build output
Basic Configuration
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
// Shared config (inherited by all environments)
build: {
sourcemap: false,
},
environments: {
// Browser client (default)
client: {
build: {
outDir: 'dist/client',
manifest: true,
},
},
// Server-side rendering (Node.js)
ssr: {
build: {
outDir: 'dist/server',
target: 'node20',
rollupOptions: {
output: { format: 'esm' },
},
},
},
// Edge runtime (Cloudflare Workers, etc.)
edge: {
resolve: {
noExternal: true, // Bundle all dependencies
conditions: ['edge', 'worker'],
},
build: {
outDir: 'dist/edge',
rollupOptions: {
external: ['cloudflare:workers'],
},
},
},
},
})Accessing Environments in Plugins
Plugins can access the current environment via this.environment:
export function myPlugin(): Plugin {
return {
name: 'my-plugin',
transform(code, id) {
// Environment available in all hooks
const env = this.environment
if (env.name === 'ssr') {
// SSR-specific transform
return transformForSSR(code)
}
if (env.name === 'edge') {
// Edge-specific transform
return transformForEdge(code)
}
// Default client transform
return transformForClient(code)
},
configureServer(server) {
// Access specific environments
const ssrEnv = server.environments.ssr
const clientEnv = server.environments.client
},
}
}Per-Environment Plugins
Use perEnvironmentPlugin helper:
import { perEnvironmentPlugin } from 'vite'
// Plugin only for SSR environment
export const ssrOnlyPlugin = perEnvironmentPlugin('ssr-only', (environment) => {
if (environment.name !== 'ssr') {
return null // Don't apply to other environments
}
return {
transform(code, id) {
// SSR-only transformation
},
}
})Builder API
For coordinated multi-environment builds:
import { createBuilder } from 'vite'
async function build() {
const builder = await createBuilder({
environments: {
client: { build: { outDir: 'dist/client' } },
ssr: { build: { outDir: 'dist/server' } },
},
})
// Build all environments in parallel
await builder.build()
// Or build individually
await builder.build(builder.environments.client)
await builder.build(builder.environments.ssr)
// Cleanup
await builder.close()
}buildApp Hook (Vite 7)
Plugins can coordinate environment builds:
export function frameworkPlugin(): Plugin {
return {
name: 'framework-plugin',
// Order: 'pre' runs before builder.buildApp, 'post' after
buildApp: {
order: 'pre',
async handler(builder) {
// Pre-build setup
await prepareAssets()
// Build specific environments
await builder.build(builder.environments.client)
// Check if environment already built
if (!builder.environments.ssr.isBuilt) {
await builder.build(builder.environments.ssr)
}
},
},
}
}Environment Instance API
// In dev server
const server = await createServer()
const clientEnv = server.environments.client
const ssrEnv = server.environments.ssr
// Transform module in specific environment
const result = await ssrEnv.transformRequest('/src/app.js')
// Hot module handling
ssrEnv.hot.send({ type: 'custom', event: 'reload' })ModuleRunner (SSR)
Execute modules in the SSR environment:
const ssrEnv = server.environments.ssr
// Import module through runner
const { render } = await ssrEnv.runner.import('/src/entry-server.js')
// Execute render
const html = await render(url)Real-World: Cloudflare Workers
The Cloudflare Vite plugin demonstrates Environment API:
// Cloudflare plugin creates a custom environment
export default defineConfig({
plugins: [cloudflare()],
environments: {
// Cloudflare Workers environment
worker: {
resolve: {
conditions: ['workerd', 'worker', 'browser'],
},
build: {
outDir: 'dist/worker',
rollupOptions: {
external: ['cloudflare:workers'],
},
},
},
},
})Node.js Requirements
Vite 7 requires:
- Node.js 20.19+ or
- Node.js 22.12+
These versions support require(esm) without a flag, enabling ESM-only distribution.
Library Mode
Vite Library Mode
Building publishable npm packages.
Basic Library Config
// vite.config.ts
import { defineConfig } from 'vite'
import { resolve } from 'path'
import react from '@vitejs/plugin-react'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
react(),
dts({ include: ['src'] }), // Generate .d.ts files
],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib', // Global variable name for UMD
fileName: (format) => `my-lib.${format}.js`,
},
rollupOptions: {
// Externalize dependencies that shouldn't be bundled
external: ['react', 'react-dom'],
output: {
// Global variables for UMD build
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
})Package.json Setup
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"main": "./dist/my-lib.umd.js",
"module": "./dist/my-lib.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/my-lib.es.js",
"require": "./dist/my-lib.umd.js",
"types": "./dist/index.d.ts"
},
"./styles.css": "./dist/style.css"
},
"files": [
"dist"
],
"sideEffects": [
"**/*.css"
],
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"vite": "^7.0.0",
"vite-plugin-dts": "^4.0.0"
},
"scripts": {
"build": "vite build",
"dev": "vite"
}
}Multiple Entry Points
// vite.config.ts
export default defineConfig({
build: {
lib: {
entry: {
index: resolve(__dirname, 'src/index.ts'),
utils: resolve(__dirname, 'src/utils/index.ts'),
hooks: resolve(__dirname, 'src/hooks/index.ts'),
},
formats: ['es', 'cjs'],
},
rollupOptions: {
external: ['react', 'react-dom'],
},
},
})With matching exports:
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.js",
"require": "./dist/utils.cjs",
"types": "./dist/utils.d.ts"
},
"./hooks": {
"import": "./dist/hooks.js",
"require": "./dist/hooks.cjs",
"types": "./dist/hooks.d.ts"
}
}
}CSS Handling
// vite.config.ts
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
},
cssCodeSplit: false, // Bundle all CSS into one file
rollupOptions: {
external: ['react', 'react-dom'],
},
},
})For CSS modules with TypeScript:
// vite.config.ts
export default defineConfig({
css: {
modules: {
localsConvention: 'camelCase',
},
},
})Preserving File Structure
// vite.config.ts
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
formats: ['es'],
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
preserveModules: true, // Keep file structure
preserveModulesRoot: 'src',
entryFileNames: '[name].js',
},
},
},
})TypeScript Declarations
Install and configure vite-plugin-dts:
npm install -D vite-plugin-dtsimport dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
dts({
include: ['src'],
exclude: ['src/**/*.test.ts', 'src/**/*.stories.tsx'],
rollupTypes: true, // Bundle .d.ts files
}),
],
})Development Testing
// vite.config.ts
export default defineConfig(({ command }) => ({
plugins: [react()],
// Only apply library config for build
...(command === 'build' && {
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
},
},
}),
}))Pre-publish Checklist
# 1. Build
npm run build
# 2. Check output
ls -la dist/
# 3. Verify types
cat dist/index.d.ts
# 4. Test locally
cd ../test-project
npm link ../my-lib
# 5. Publish
npm publishPlugin Development
Vite Plugin Development
Creating custom Vite plugins.
Basic Plugin Structure
import type { Plugin } from 'vite'
export function myPlugin(options?: { debug?: boolean }): Plugin {
return {
name: 'my-plugin', // Required: unique plugin name
// Hooks listed in execution order
config(config, env) {
// Modify config before resolution
return {
define: {
__MY_PLUGIN__: JSON.stringify(true),
},
}
},
configResolved(config) {
// Called once config is fully resolved
if (options?.debug) {
console.log('Resolved config:', config)
}
},
configureServer(server) {
// Add middleware or modify dev server
server.middlewares.use((req, res, next) => {
if (req.url === '/my-plugin-endpoint') {
res.end('Hello from plugin')
return
}
next()
})
},
buildStart() {
// Called at build start
},
resolveId(id) {
// Custom module resolution
if (id === 'virtual:my-module') {
return '\0virtual:my-module'
}
},
load(id) {
// Load virtual modules
if (id === '\0virtual:my-module') {
return `export const data = ${JSON.stringify({ version: '1.0' })}`
}
},
transform(code, id) {
// Transform individual modules
if (id.endsWith('.custom')) {
return {
code: transformCustomFormat(code),
map: null,
}
}
},
buildEnd() {
// Called after build completes
},
closeBundle() {
// Cleanup after bundle is written
},
}
}Hook Execution Order
1. config - Modify/extend config
2. configResolved - Access final config
3. configureServer - Dev server setup (dev only)
4. buildStart - Build begins
5. resolveId - Resolve import paths
6. load - Load module content
7. transform - Transform module code
8. buildEnd - Build complete
9. closeBundle - Bundle writtenVirtual Modules
Generate modules at runtime:
const virtualModuleId = 'virtual:my-data'
const resolvedVirtualModuleId = '\0' + virtualModuleId
export function virtualDataPlugin(data: Record<string, unknown>): Plugin {
return {
name: 'virtual-data',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `export default ${JSON.stringify(data)}`
}
},
}
}
// Usage in app:
// import data from 'virtual:my-data'Transform Hook Patterns
export function transformPlugin(): Plugin {
return {
name: 'transform-plugin',
transform(code, id) {
// Only transform specific files
if (!id.endsWith('.special.ts')) {
return null
}
// Return transformed code
return {
code: code.replace(/PLACEHOLDER/g, 'REPLACED'),
map: null, // Or source map
}
},
}
}Using Rollup Plugins
Many Rollup plugins work in Vite:
import { defineConfig } from 'vite'
import commonjs from '@rollup/plugin-commonjs'
export default defineConfig({
plugins: [
// Rollup plugins work in Vite
commonjs(),
],
})Environment-Aware Plugins (Vite 7)
export function envAwarePlugin(): Plugin {
return {
name: 'env-aware',
transform(code, id) {
// Access current environment
const env = this.environment
if (env.name === 'ssr') {
return transformForServer(code)
}
return transformForClient(code)
},
}
}Applying Plugins Conditionally
export function conditionalPlugin(): Plugin {
let isDev: boolean
return {
name: 'conditional',
configResolved(config) {
isDev = config.command === 'serve'
},
transform(code, id) {
if (isDev) {
// Dev-only transformation
return injectDevHelpers(code)
}
return null
},
}
}Plugin Ordering
export function orderedPlugin(): Plugin {
return {
name: 'ordered',
enforce: 'pre', // Run before core plugins
// enforce: 'post' // Run after core plugins
}
}Hot Module Replacement
export function hmrPlugin(): Plugin {
return {
name: 'hmr-plugin',
handleHotUpdate({ file, server }) {
if (file.endsWith('.custom')) {
// Custom HMR handling
server.ws.send({
type: 'custom',
event: 'custom-update',
data: { file },
})
return [] // Prevent default HMR
}
},
}
}
// Client-side HMR handling:
// if (import.meta.hot) {
// import.meta.hot.on('custom-update', (data) => {
// console.log('Custom file updated:', data.file)
// })
// }TypeScript Declarations
// Type declarations for virtual module
declare module 'virtual:my-data' {
const data: { version: string }
export default data
}Ssr Configuration
Vite SSR Configuration
Server-side rendering setup for development and production.
Project Structure
project/
├── index.html
├── src/
│ ├── main.tsx # Client entry
│ ├── entry-client.tsx # Client-specific setup
│ ├── entry-server.tsx # Server render function
│ └── App.tsx
├── server.js # Production server
└── vite.config.tsEntry Points
Client Entry (entry-client.tsx)
import { hydrateRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
hydrateRoot(
document.getElementById('root')!,
<BrowserRouter>
<App />
</BrowserRouter>
)Server Entry (entry-server.tsx)
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom/server'
import App from './App'
export function render(url: string) {
return renderToString(
<StaticRouter location={url}>
<App />
</StaticRouter>
)
}Development Server
// server-dev.js
import fs from 'node:fs'
import path from 'node:path'
import express from 'express'
import { createServer as createViteServer } from 'vite'
async function createServer() {
const app = express()
// Create Vite server in middleware mode
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
})
// Use Vite's middleware
app.use(vite.middlewares)
app.use('*', async (req, res, next) => {
const url = req.originalUrl
try {
// 1. Read index.html
let template = fs.readFileSync(
path.resolve('index.html'),
'utf-8'
)
// 2. Apply Vite HTML transforms (HMR client, etc.)
template = await vite.transformIndexHtml(url, template)
// 3. Load server entry with HMR support
const { render } = await vite.ssrLoadModule('/src/entry-server.tsx')
// 4. Render app HTML
const appHtml = await render(url)
// 5. Inject into template
const html = template.replace('<!--ssr-outlet-->', appHtml)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
vite.ssrFixStacktrace(e as Error)
next(e)
}
})
app.listen(5173)
}
createServer()Production Build
Build Scripts (package.json)
{
"scripts": {
"dev": "node server-dev.js",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outDir dist/server --ssr src/entry-server.tsx",
"preview": "node server-prod.js"
}
}Production Server
// server-prod.js
import fs from 'node:fs'
import path from 'node:path'
import express from 'express'
const app = express()
// Serve static assets
app.use(express.static('dist/client', { index: false }))
app.use('*', async (req, res) => {
const url = req.originalUrl
// Read pre-built template
const template = fs.readFileSync(
path.resolve('dist/client/index.html'),
'utf-8'
)
// Import pre-built server bundle
const { render } = await import('./dist/server/entry-server.js')
const appHtml = await render(url)
const html = template.replace('<!--ssr-outlet-->', appHtml)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
})
app.listen(3000)Vite Config for SSR
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
// Shared build options
sourcemap: true,
},
// SSR-specific options
ssr: {
// Externalize dependencies (not bundled)
external: ['express'],
// Force bundle specific packages
noExternal: ['some-ssr-unfriendly-package'],
},
})Vite 7 Environment API (Alternative)
export default defineConfig({
environments: {
client: {
build: {
outDir: 'dist/client',
manifest: true,
},
},
ssr: {
build: {
outDir: 'dist/server',
ssr: 'src/entry-server.tsx',
target: 'node20',
},
},
},
})Streaming SSR
// entry-server.tsx with streaming
import { renderToPipeableStream } from 'react-dom/server'
export function render(url: string, res: Response) {
const { pipe, abort } = renderToPipeableStream(
<StaticRouter location={url}>
<App />
</StaticRouter>,
{
onShellReady() {
res.setHeader('Content-Type', 'text/html')
pipe(res)
},
onError(error) {
console.error(error)
},
}
)
setTimeout(abort, 10000) // Timeout
}index.html Template
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SSR App</title>
</head>
<body>
<div id="root"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.tsx"></script>
</body>
</html>Checklists (1)
Production Build
Vite Production Build Checklist
Pre-deployment build verification.
Configuration
-
build.targetset appropriately for audience -
build.sourcemapdisabled or set to'hidden' -
build.minifyenabled (esbuild or terser) - Environment variables properly defined
Bundle Optimization
- Run bundle analyzer:
npx vite-bundle-visualizer - No unexpected large chunks (> 500kb)
- Vendor chunks split appropriately
- No duplicate dependencies
- Tree shaking working (check for dead code)
Chunk Strategy
// Recommended manual chunks
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'router': ['react-router-dom'],
'ui': ['@radix-ui/*'],
}- React/framework in separate vendor chunk
- Route-based code splitting enabled
- Large libraries in separate chunks
Assets
- Images optimized (WebP, AVIF)
-
assetsInlineLimitset appropriately (default 4kb) - Static assets in
public/folder - Asset filenames include hash for caching
CSS
- CSS minified
- CSS code split by entry point (if needed)
- No unused CSS (PurgeCSS or similar)
- PostCSS processing complete
Environment Variables
-
.env.productionexists with production values - No secrets in client-side env vars
-
VITE_*prefix used for client vars - Build-time vars validated
TypeScript
-
tsc --noEmitpasses - No type errors in build
- Source maps working if enabled
SSR (If Applicable)
- Server entry point builds correctly
- External dependencies configured
- Client manifest generated
- Hydration working
Testing
- Build completes without errors
- Preview mode works:
vite preview - All routes accessible
- No console errors in production build
- Performance metrics acceptable
Output Verification
# Check build output
ls -la dist/
# Check chunk sizes
du -sh dist/assets/*
# Preview locally
vite preview
# Test production server
NODE_ENV=production node server.jsPerformance Targets
| Metric | Target | Current |
|---|---|---|
| Initial JS | < 200kb gzipped | ___ |
| Main chunk | < 150kb | ___ |
| Vendor chunk | < 100kb | ___ |
| CSS | < 50kb | ___ |
| LCP | < 2.5s | ___ |
| TTI | < 3.5s | ___ |
Pre-Deploy Commands
# Full build
npm run build
# Type check
npm run typecheck
# Preview
npm run preview
# Analyze bundle
npx vite-bundle-visualizerCommon Issues
Large Bundle
- Check for duplicate dependencies
- Ensure tree shaking is working
- Split vendor chunks
- Use dynamic imports
Build Failures
- Clear
.vitecache:rm -rf node_modules/.vite - Check TypeScript errors
- Verify all imports resolve
Missing Assets
- Check
public/folder structure - Verify asset paths in code
- Check
baseconfig if using subpath
Sign-Off
- Build size within budget
- Preview works correctly
- No console errors
- Performance acceptable
- Ready for deployment
Verify
Comprehensive verification with parallel test agents. Use when verifying implementations or validating changes.
Web Research Workflow
Unified decision tree for web research and competitive monitoring. Auto-selects WebFetch, Tavily, or agent-browser based on target site characteristics and available API keys. Includes competitor page tracking, snapshot diffing, and change alerting. Use when researching web content, scraping, extracting raw markdown, capturing documentation, or monitoring competitor changes.
Last updated on