vite Best Practices
Vite is the modern standard for frontend tooling. Adhere to these principles to leverage its full potential, ensuring blazing-fast development and optimized production builds.
1. Code Organization and Structure
Keep vite.config.js Minimal
Vite's philosophy is a lean core. Avoid over-configuring. Only add plugins or options when absolutely necessary.
❌ BAD - Overly complex vite.config.js
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import legacy from '@vitejs/plugin-legacy';
import { visualizer } from 'rollup-plugin-visualizer';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
react(),
legacy({ targets: ['defaults', 'not IE 11'] }),
visualizer({ filename: './dist/stats.html' }),
VitePWA({ registerType: 'autoUpdate' }),
// ... many more plugins
],
resolve: {
alias: {
'@': '/src',
'~': '/node_modules',
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
},
build: {
target: 'es2015',
minify: 'terser',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
server: {
port: 3000,
open: true,
proxy: {
'/api': 'http://localhost:8080',
},
},
});
✅ GOOD - Lean and focused vite.config.js
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
// Only add resolve.alias if absolutely needed for complex paths.
// Avoid resolve.extensions unless you have specific non-standard file types.
// Vite's defaults are usually sufficient.
});
Use Explicit File Extensions
Relying on resolve.extensions for implicit imports forces Vite to perform multiple filesystem checks, slowing down resolution. Be explicit.
❌ BAD - Implicit import
// src/components/MyComponent.jsx
import { util } from '../utils'; // Vite has to guess .js, .ts, .jsx etc.
✅ GOOD - Explicit import
// src/components/MyComponent.jsx
import { util } from '../utils/index.js'; // Or .ts, .jsx, etc.
Avoid Barrel Files
Barrel files (e.g., index.js re-exporting many modules) force Vite to fetch and transform all re-exported files, even if only one API is used. This hurts initial page load performance.
❌ BAD - Barrel file (src/utils/index.js)
// src/utils/index.js
export * from './color.js';
export * from './dom.js';
export * from './slash.js';
// src/app.js
import { slash } from './utils'; // Loads color.js, dom.js, and slash.js
✅ GOOD - Direct imports
// src/app.js
import { slash } from './utils/slash.js'; // Only loads slash.js
2. Common Patterns and Anti-patterns
Embrace Native ES Modules
Vite is built on native ES Modules. Always write your client-side code using import/export syntax.
❌ BAD - CommonJS in client-side code
// main.js
const myModule = require('./my-module'); // Will fail in browser
✅ GOOD - Native ES Modules
// main.js
import myModule from './my-module.js';
Use import.meta.env for Environment Variables
Vite injects environment variables via import.meta.env. This is the correct way to access them in client-side code. process.env is for Node.js environments.
❌ BAD - Using process.env in client code
// app.js
console.log(process.env.VITE_API_URL); // `process` is not defined in browser
✅ GOOD - Using import.meta.env
// app.js
console.log(import.meta.env.VITE_API_URL); // Correctly accesses Vite env vars
Optimize with Dynamic Imports
For large components or libraries, use dynamic imports to load them only when needed, reducing initial bundle size and improving load times.
❌ BAD - Eagerly loading large component
// App.jsx
import LargeComponent from './LargeComponent'; // Always bundled
function App() {
return <LargeComponent />;
}
✅ GOOD - Dynamically importing
// App.jsx (React example)
import { lazy, Suspense } from 'react';
const LargeComponent = lazy(() => import('./LargeComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LargeComponent />
</Suspense>
);
}
3. Performance Considerations
Audit Custom Plugins
Community plugins can introduce performance bottlenecks. Profile them using Vite's debug flags.
❌ BAD - Blindly adding plugins
// vite.config.js
import { defineConfig } from 'vite';
import someHeavyPlugin from 'some-heavy-plugin'; // No profiling done
export default defineConfig({
plugins: [someHeavyPlugin()],
});
✅ GOOD - Profiling plugins
# Run Vite with debug flags to identify slow plugins
vite --debug plugin-transform
Use vite-plugin-inspect to visualize the transform pipeline.
Optimize Browser Setup
Browser extensions and disabled cache settings can severely impact dev server performance.
❌ BAD - Developing with "Disable Cache" enabled in dev tools.
// Browser Dev Tools -> Network tab -> "Disable Cache" checked
✅ GOOD - Use a clean browser profile or incognito mode. Ensure "Disable Cache" is unchecked in dev tools.
Warm Up Critical Files
For complex applications, pre-warming frequently used files can prevent request waterfalls.
❌ BAD - Relying solely on on-demand transformation for critical paths.
// No explicit warmup configured
✅ GOOD - Use server.warmup in vite.config.js
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
server: {
warmup: {
clientFiles: ['./src/main.js', './src/App.jsx'],
// Or use patterns: ['**/*.vue', '**/*.jsx']
},
},
});
4. Common Pitfalls and Gotchas
Incorrect Base Path for Deployment
When deploying to a sub-path (e.g., yourdomain.com/my-app/), ensure base is correctly configured.
❌ BAD - Hardcoding absolute paths or missing base
// vite.config.js
// Default base: '/'
// Assets might break when deployed to a sub-path
✅ GOOD - Configure base for sub-path deployments
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
base: '/my-app/', // For deploying to https://yourdomain.com/my-app/
// Or use './' for relative paths if the base is unknown at build time
// base: './',
});
Access the base path in your code via import.meta.env.BASE_URL.
Mismanaging NODE_ENV with API Usage
When using Vite's JS API (createServer, build) in the same Node.js process, ensure process.env.NODE_ENV or the mode config option is consistent to prevent conflicts.
❌ BAD - Conflicting NODE_ENV
// script.js
process.env.NODE_ENV = 'production';
await createServer(); // Might behave unexpectedly
✅ GOOD - Explicitly set mode or spawn child processes
// script.js
import { createServer } from 'vite';
// Option 1: Explicitly set mode
const devServer = await createServer({ mode: 'development' });
await devServer.listen();
// Option 2: Spawn child processes for separate contexts
// (e.g., one for dev server, one for build)
5. Testing Approaches
Standardize on Vitest
Vitest is the official testing framework for Vite projects, offering seamless integration with Vite's configuration and plugin ecosystem.
❌ BAD - Using a separate test runner (e.g., Jest) that requires its own complex configuration.
// package.json
"scripts": {
"test": "jest" // Requires separate Babel/Webpack config
}
✅ GOOD - Integrate Vitest directly into vite.config.ts
// vite.config.ts
/// <reference types="vitest/config" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true, // For global APIs like `describe`, `it`, `expect`
environment: 'jsdom', // Or 'node'
setupFiles: './src/setupTests.js', // Global setup for tests
},
});