Vite Complete Guide | Fast Dev Server, Build Optimization, Plugins & Library Mode
이 글의 핵심
Vite starts a dev server in under 300ms and uses native ESM to avoid bundling during development — resulting in near-instant Hot Module Replacement regardless of project size. This guide covers everything from basic setup to plugin authoring and library mode.
Why Vite?
webpack dev server startup (large project): 15-60 seconds
Vite dev server startup: ~300ms
webpack HMR (large project): 2-10 seconds
Vite HMR: <100ms (often <50ms)
Vite achieves this by eliminating the bundling step during development:
webpack: source → bundle all modules → serve bundle → browser runs bundle
Vite: source → serve files directly → browser imports via native ESM
(browser only loads what it actually needs)
Quick Start
# React + TypeScript
npm create vite@latest my-app -- --template react-ts
cd my-app && npm install && npm run dev
# Vue + TypeScript
npm create vite@latest my-app -- --template vue-ts
# Vanilla TypeScript
npm create vite@latest my-app -- --template vanilla-ts
Available templates: vanilla, vanilla-ts, vue, vue-ts, react, react-ts, react-swc, react-swc-ts, preact, preact-ts, lit, lit-ts, svelte, svelte-ts, solid, solid-ts
Project Structure
my-app/
├── public/ # Static assets (served as-is, no processing)
│ └── favicon.ico
├── src/
│ ├── main.tsx # Entry point
│ ├── App.tsx
│ └── assets/ # Imported assets (processed by Vite)
├── index.html # Entry HTML (not in public/ — Vite processes it)
├── vite.config.ts
├── tsconfig.json
└── package.json
<!-- index.html — Vite processes this -->
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<!-- Entry point referenced directly — Vite handles bundling -->
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
// Path aliases
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@hooks': path.resolve(__dirname, './src/hooks'),
},
},
// Dev server
server: {
port: 3000,
open: true, // Open browser on start
// Proxy API calls to backend (avoids CORS in dev)
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
// Build settings
build: {
outDir: 'dist',
sourcemap: true, // Source maps for debugging
minify: 'esbuild', // Fast minification
target: 'es2020', // Browser target
rollupOptions: {
output: {
// Code splitting: split vendor libraries into separate chunk
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
},
},
},
},
// CSS
css: {
modules: {
localsConvention: 'camelCase', // CSS Module class names as camelCase
},
preprocessorOptions: {
scss: {
additionalData: '@import "@/styles/variables.scss";', // Global SCSS
},
},
},
});
Environment Variables
Vite uses .env files. Variables must be prefixed with VITE_ to be exposed to the browser:
# .env (committed — default values)
VITE_APP_NAME=MyApp
VITE_API_URL=http://localhost:8000
# .env.local (not committed — overrides .env locally)
VITE_API_URL=http://localhost:3001
# .env.production (used when building for production)
VITE_API_URL=https://api.myapp.com
# .env.development (used during dev server)
VITE_DEBUG=true
// Access in your code
const apiUrl = import.meta.env.VITE_API_URL;
const appName = import.meta.env.VITE_APP_NAME;
// Built-in Vite env vars
const isDev = import.meta.env.DEV; // true in dev
const isProd = import.meta.env.PROD; // true in production
const mode = import.meta.env.MODE; // 'development' | 'production'
// TypeScript: add types for your env vars
// src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_NAME: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Asset Handling
// Import images — Vite returns the URL
import logoUrl from './logo.svg';
import heroUrl from './hero.png';
function App() {
return <img src={logoUrl} alt="Logo" />;
}
// Import as raw string
import svgRaw from './icon.svg?raw';
document.body.innerHTML = svgRaw;
// Import as URL explicitly
import fileUrl from './data.json?url';
// Import as inline base64 (small files)
import inlined from './small-icon.png?inline';
// Dynamic import (code splitting)
const { default: Chart } = await import('./Chart');
// vite.config.ts — configure asset handling
export default defineConfig({
build: {
assetsInlineLimit: 4096, // Inline assets < 4KB as base64 (default)
},
});
Hot Module Replacement (HMR)
Vite’s HMR updates only the changed module — not the whole page.
// Opt into HMR in custom modules
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// Called when this module or its deps update
console.log('Module updated:', newModule);
});
import.meta.hot.dispose(() => {
// Clean up before the module is replaced
clearInterval(timer);
});
}
For React, @vitejs/plugin-react uses React Refresh — components update without losing state.
Plugins
Official Plugins
npm install -D @vitejs/plugin-react # React with Babel transforms
npm install -D @vitejs/plugin-react-swc # React with SWC (faster)
npm install -D @vitejs/plugin-vue # Vue 3
npm install -D @vitejs/plugin-legacy # Polyfills for older browsers
Popular Community Plugins
# Auto-import — import React, useState, etc. without explicit import
npm install -D unplugin-auto-import
# SVG as React components
npm install -D vite-plugin-svgr
# Bundle analysis
npm install -D rollup-plugin-visualizer
# PWA support
npm install -D vite-plugin-pwa
// vite.config.ts with plugins
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
svgr({
svgrOptions: { icon: true },
}),
visualizer({
filename: 'dist/stats.html', // Open after build to see bundle breakdown
open: true,
gzipSize: true,
}),
],
});
Writing a Custom Plugin
// plugins/my-plugin.ts
import type { Plugin } from 'vite';
export function myPlugin(): Plugin {
return {
name: 'my-plugin',
// Transform source files
transform(code, id) {
if (!id.endsWith('.ts')) return null;
// Replace __BUILD_TIME__ with actual timestamp
return code.replace(/__BUILD_TIME__/g, Date.now().toString());
},
// Inject HTML
transformIndexHtml(html) {
return html.replace(
'<head>',
`<head>\n <meta name="build-time" content="${Date.now()}" />`
);
},
// Handle virtual modules
resolveId(id) {
if (id === 'virtual:my-module') return id;
},
load(id) {
if (id === 'virtual:my-module') {
return `export const message = 'Hello from virtual module!';`;
}
},
};
}
Library Mode
Build a reusable library instead of an app:
// vite.config.ts — library mode
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
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: 'MyLibrary',
fileName: (format) => `my-library.${format}.js`,
formats: ['es', 'cjs'], // ESM + CommonJS
},
rollupOptions: {
// Exclude peer dependencies from bundle
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
});
// package.json for the library
{
"name": "my-library",
"main": "./dist/my-library.cjs.js",
"module": "./dist/my-library.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/my-library.es.js",
"require": "./dist/my-library.cjs.js"
}
}
}
Multi-Page App (MPA)
// vite.config.ts — multiple entry points
export default defineConfig({
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
admin: resolve(__dirname, 'admin/index.html'),
login: resolve(__dirname, 'login/index.html'),
},
},
},
});
Migration from webpack
// Common webpack → Vite equivalents
// webpack: require('./image.png')
// Vite: import imageUrl from './image.png'
// webpack: require.context
// Vite: import.meta.glob
// Glob import all files matching a pattern
const modules = import.meta.glob('./routes/*.tsx');
// Returns: { './routes/home.tsx': () => import('./routes/home.tsx'), ... }
// Eager loading (synchronous)
const modules = import.meta.glob('./routes/*.tsx', { eager: true });
// webpack: process.env.REACT_APP_*
// Vite: import.meta.env.VITE_*
// webpack: DefinePlugin
// Vite: define in vite.config.ts
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
},
});
npm run preview — Test Production Build
npm run build # Build production output to dist/
npm run preview # Serve the dist/ folder locally on port 4173
Always test the production build before deploying — dev and prod can behave differently (env vars, asset paths, code splitting).
Quick Reference
| Task | Config |
|---|---|
| Dev server port | server: { port: 3000 } |
| API proxy | server: { proxy: { '/api': 'http://localhost:8000' } } |
| Path alias | resolve: { alias: { '@': './src' } } |
| Source maps | build: { sourcemap: true } |
| Code splitting | build: { rollupOptions: { output: { manualChunks: {...} } } } |
| Global SCSS | css: { preprocessorOptions: { scss: { additionalData: '...' } } } |
| Analyze bundle | rollup-plugin-visualizer |
Related posts: