Vite Features & Optimization
Vite provides HMR, code splitting, library mode, and multiple build targets. This page covers advanced configuration for production-ready applications.
HMR — Hot Module Replacement
// Vite's HMR API — accept updates for a module
if (import.meta.hot) {
// Accept self — when this module changes, re-execute it
import.meta.hot.accept((newModule) => {
if (newModule) {
newModule.render();
}
});
// Accept updates from a dependency
import.meta.hot.accept('./dep.ts', (newDep) => {
console.log('dep updated', newDep);
});
// Cleanup side effects before module is replaced
import.meta.hot.dispose((data) => {
data.intervalId = setInterval(() => {}, 1000);
});
// Decline HMR for this module — full page reload instead
import.meta.hot.decline();
// Invalidate — force full reload
import.meta.hot.invalidate('module state is stale');
}
// HMR data — persist data across updates
import.meta.hot.data.count ??= 0;
import.meta.hot.data.count++;Code Splitting & Dynamic Imports
// Dynamic import — creates a separate chunk automatically
const { default: Chart } = await import('./Chart');
// React lazy + Suspense
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Prefetch hint — loads chunk in background
const prefetch = () => import(/* @vite-ignore */ './HeavyPage');
onMouseEnter={() => prefetch()};Build Optimization & Manual Chunks
// vite.config.ts — advanced build optimization
build: {
rollupOptions: {
output: {
// Manual chunk splitting
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react')) return 'react-vendor';
if (id.includes('@radix-ui')) return 'radix-vendor';
if (id.includes('framer-motion')) return 'animation-vendor';
return 'vendor'; // everything else into vendor
}
},
// Or use object form for explicit grouping:
// manualChunks: {
// vendor: ['react', 'react-dom', 'react-router-dom'],
// },
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash][extname]',
},
},
// Dependency pre-bundling control
optimizeDeps: {
include: ['lodash-es', 'date-fns'], // force pre-bundle
exclude: ['your-local-lib'], // skip pre-bundling
},
}Library Mode
// Build a reusable library (component library, SDK)
import { resolve } from 'path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts'; // generates .d.ts declarations
export default defineConfig({
plugins: [dts({ include: ['src'] })],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
formats: ['es', 'cjs', 'umd'], // output formats
fileName: (format) => `my-lib.${format}.js`,
},
rollupOptions: {
// Externalize peer deps — don't bundle React into the lib
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
});
// package.json for the library
// {
// "main": "./dist/my-lib.cjs.js",
// "module": "./dist/my-lib.es.js",
// "types": "./dist/index.d.ts",
// "exports": {
// ".": {
// "import": "./dist/my-lib.es.js",
// "require": "./dist/my-lib.cjs.js"
// }
// }
// }Vitest — Co-located Test Runner
// Vitest config inside vite.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true, // no need to import describe/it/expect
environment: 'jsdom', // 'node' | 'jsdom' | 'happy-dom'
setupFiles: ['./src/test/setup.ts'],
include: ['**/*.{test,spec}.{ts,tsx}'],
exclude: ['node_modules', 'dist', 'e2e'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
thresholds: { lines: 80, branches: 75 },
},
// Mock CSS modules and static assets
css: false,
},
});
// src/test/setup.ts
import '@testing-library/jest-dom';
// Example test
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from '@/components/Button';
describe('Button', () => {
it('calls onClick when clicked', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledOnce();
});
});
// CLI
// vitest — watch mode
// vitest run — single run (CI)
// vitest --ui — browser UI
// vitest --coverage — with coverageKeep your own version of these notes — editable, searchable, and organised by your stack.
Start free