Vite from Beginner to Expert: A Learning Path
If you have ever stared at a blank terminal while your bundler churns through thousands of modules, you already understand the problem Vite was built to solve. Vite, created by Evan You (the creator of Vue.js), is a next-generation frontend build tool that combines an extremely fast dev server with an optimized production build pipeline. This tutorial will take you from absolute beginner to confident expert, covering everything from installation to advanced configuration, plugins, SSR, and deployment best practices.
What Is Vite?
Vite (pronounced "veet," the French word for "fast") is a build tool and dev server for modern web projects. It consists of two distinct parts:
- A dev server that serves your source files over native ES modules (ESM), providing instant server start and lightning-fast Hot Module Replacement (HMR).
- A build command that bundles your code with Rollup, producing highly optimized static assets for production.
Unlike traditional bundlers such as Webpack or Parcel, which bundle everything before serving, Vite serves source files directly in development. This means the dev server start time is largely independent of the size of your project.
Why Vite Matters
Modern web applications often contain thousands of modules. With traditional bundlers, every change triggers a rebuild of the dependency graph, which can take seconds or even minutes on large projects. Vite changes this equation in several important ways:
- Instant server startup: Vite only processes the modules you actually request, so cold starts happen in milliseconds.
- Fast HMR: Hot Module Replacement is scoped to the exact module that changed, regardless of app size.
- Rich features out of the box: TypeScript, JSX, CSS pre-processors, CSS code splitting, and asset handling are built in.
- Framework agnostic: While created by the Vue team, Vite works equally well with React, Svelte, Preact, Lit, and vanilla JS.
- Optimized production builds: Vite uses Rollup under the hood, producing smaller, more efficient bundles than many alternatives.
Getting Started: Your First Vite Project
The fastest way to start is with the official scaffolding tool. Make sure you have Node.js version 18 or higher installed, then run:
npm create vite@latest my-first-app
You will be prompted to choose a framework (Vanilla, Vue, React, Svelte, Preact, Lit) and a variant (JavaScript or TypeScript). Once selected, navigate into the project and install dependencies:
cd my-first-app
npm install
npm run dev
Open your browser at http://localhost:5173 and you will see your app running. Try editing src/main.js — the change appears instantly without a full page reload.
Understanding the Project Structure
A typical Vite project looks like this:
my-first-app/
├── index.html
├── package.json
├── vite.config.js
├── public/
│ └── vite.svg
└── src/
├── main.js
├── App.vue (or App.jsx, etc.)
└── style.css
Notice that index.html lives at the project root, not inside public or src. This is intentional — Vite treats index.html as the entry point of your application. Inside it, you will find a script tag pointing to your source:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
The type="module" attribute is what enables native ESM in the browser during development.
Core Concepts You Must Understand
Native ES Modules in Development
When you run npm run dev, Vite does not bundle your code. Instead, it transforms files on demand and serves them as native ES modules. When the browser requests main.js, Vite returns it with imports intact. The browser then requests each imported module, and Vite transforms those on the fly.
This is why startup is fast: Vite only processes what the browser actually loads.
Dependency Pre-Bundling
Some dependencies are not shipped as ESM, or they contain hundreds of internal modules that would cause excessive HTTP requests. To handle this, Vite uses esbuild to pre-bundle dependencies into a single ESM module. This happens automatically the first time you run the dev server.
You can see this in action by checking the node_modules/.vite directory after starting your project. If you ever need to force re-bundling, delete this directory or run:
npx vite optimize --force
Hot Module Replacement (HMR)
HMR allows Vite to update modules in the browser without a full reload, preserving application state. Vite provides HMR APIs that framework plugins use to implement precise updates. For example, when you edit a Vue Single File Component, only that component re-renders — your Vuex store, router state, and form inputs remain intact.
If you are writing your own module and want to accept HMR updates manually, you can use the import.meta.hot API:
export let data = { count: 0 };
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
if (newModule) {
// Preserve state across updates
data = newModule.data;
}
});
}
Configuration: Mastering vite.config.js
While Vite works with zero configuration, real projects need customization. Create a vite.config.js file at your project root:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
root: '.',
base: '/my-app/',
resolve: {
alias: {
'@': '/src',
components: '/src/components'
}
},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'dayjs']
}
}
}
}
});
Key Configuration Options Explained
- plugins: An array of Vite plugins that extend functionality.
- base: The base public path when served in production. Useful for deploying to subdirectories.
- resolve.alias: Lets you create shortcuts for imports, so
import Button from '@/components/Button'works. - server.proxy: Forwards certain requests to a backend during development, avoiding CORS issues.
- build.rollupOptions.output.manualChunks: Splits vendor code into separate files for better caching.
Environment Variables
Vite exposes environment variables prefixed with VITE_ to your client code. Define them in a .env file:
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My Awesome App
Access them anywhere in your code:
const apiUrl = import.meta.env.VITE_API_URL;
console.log(import.meta.env.VITE_APP_TITLE);
You can also use mode-specific files like .env.development and .env.production. Vite automatically loads the correct file based on the current mode.
Working with Frameworks
React
To use React, install the official plugin:
npm install @vitejs/plugin-react
Then add it to your config:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()]
});
The plugin provides Fast Refresh for React components, JSX transformation, and automatic React runtime injection.
Vue
Vue support is built into the official scaffolding, but if you are adding it manually:
npm install @vitejs/plugin-vue
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()]
});
Svelte
npm install @sveltejs/vite-plugin-svelte
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()]
});
CSS and Asset Handling
Vite has excellent built-in support for CSS. You can import CSS directly in JavaScript, and Vite handles extraction and minification automatically:
import './style.css';
CSS Pre-processors
For Sass or Less, simply install the pre-processor and import files with the correct extension:
npm install sass
import './styles/main.scss';
CSS Modules
Name your file *.module.css to enable CSS Modules:
import styles from './Button.module.css';
export function Button() {
return <button className={styles.primary}>Click me</button>;
}
PostCSS
If a postcss.config.js file exists, Vite automatically applies PostCSS to all CSS. This is useful for Autoprefixer and Tailwind CSS:
// postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
Static Assets
Importing assets returns the resolved public URL:
import logoUrl from './assets/logo.png';
export function Header() {
return <img src={logoUrl} alt="Logo" />;
}
Small assets (under 4kb by default) are inlined as base64 data URLs. You can adjust this threshold with the build.assetsInlineLimit option.
Plugins: Extending Vite
Vite's plugin API is compatible with Rollup's plugin interface, meaning many Rollup plugins work directly. A Vite plugin is simply an object with hooks:
// vite-plugin-banner.js
export default function bannerPlugin(message) {
return {
name: 'vite-plugin-banner',
transformIndexHtml(html) {
return html.replace(
'</head>',
`<!-- ${message} -->\n</head>`
);
}
};
}
Use it in your config:
import bannerPlugin from './vite-plugin-banner';
export default defineConfig({
plugins: [bannerPlugin('Built with Vite')]
});
Common Plugin Hooks
- config: Modify the Vite config before it is resolved.
- configResolved: Read the final resolved config.
- transform: Transform individual module source code.
- load: Load a module's content yourself.
- resolveId: Customize how imports are resolved.
- transformIndexHtml: Modify the HTML entry point.
- configureServer: Add custom middleware to the dev server.
Useful Community Plugins
vite-plugin-pwa: Adds Progressive Web App support with service workers.vite-plugin-compression: Generates gzip and brotli compressed assets.vite-plugin-svg-icons: Creates an SVG sprite from icon files.vite-plugin-pages: File-based routing for React or Vue.unplugin-vue-components/unplugin-react-components: Auto-imports components on demand.
Production Builds
When you are ready to deploy, run:
npm run build
Vite uses Rollup to produce an optimized bundle in the dist directory. The build process includes:
- Tree shaking to eliminate dead code.
- Code splitting for lazy-loaded routes.
- Asset hashing for long-term caching.
- CSS extraction and minification.
- HTML minification.
Previewing the Production Build
To test your production build locally before deploying:
npm run preview
This serves the dist folder on a local server so you can verify everything works as expected.
Code Splitting and Lazy Loading
Vite supports dynamic imports out of the box. Use them for route-level code splitting:
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
export function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Each lazy import becomes a separate chunk, loaded only when the user navigates to that route.
Advanced Topics
Server-Side Rendering (SSR)
Vite has built-in SSR support. The basic idea is to create a server entry point alongside your client entry point. Here is a minimal example:
// server.js
import express from 'express';
import { createServer as createViteServer } from 'vite';
async function createServer() {
const app = express();
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom'
});
app.use(vite.middlewares);
app.use('*', async (req, res) => {
try {
const template = await vite.transformIndexHtml(
req.originalUrl,
await vite.ssrLoadModule('/src/entry-server.js')
);
// Render your app to HTML here
res.status(200).set({ 'Content-Type': 'text/html' }).end(template);
} catch (e) {
vite.ssrFixStacktrace(e);
console.error(e);
res.status(500).end(e.message);
}
});
app.listen(3000);
}
createServer();
For production, you build both a client bundle and an SSR bundle, then run the server with Node.js. Frameworks like Nuxt and SvelteKit build on top of Vite's SSR capabilities to provide a complete solution.
Library Mode
If you are building a library rather than an application, Vite has a dedicated library mode:
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: 'src/my-lib.js',
name: 'MyLib',
fileName: (format) => `my-lib.${format}.js`
},
rollupOptions: {
external: ['react', 'react-dom']
}
}
});
This produces UMD, ESM, and CommonJS bundles, with peer dependencies marked as external.
Multi-Page Applications
For apps with multiple HTML entry points, configure Rollup to know about each page:
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
about: resolve(__dirname, 'about/index.html'),
blog: resolve(__dirname, 'blog/index.html')
}
}
}
});
TypeScript Configuration
Vite supports TypeScript out of the box using esbuild for transpilation, which is extremely fast. However, esbuild does not perform type checking — it only strips types. For type checking, run tsc separately:
{
"scripts": {
"build": "tsc && vite build",
"dev": "vite"
}
}
Add Vite's client types to your tsconfig.json to get proper typing for import.meta.env and asset imports:
{
"compilerOptions": {
"types": ["vite/client"]
}
}
Path Aliases with TypeScript
If you use path aliases in Vite, mirror them in TypeScript so the compiler understands them:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
Best Practices
1. Keep Dependencies Lean
Every dependency adds to your bundle size and pre-bundling time. Regularly audit your dependencies with tools like npm ls or bundlephobia. Prefer tree-shakeable libraries and consider lighter alternatives (for example, dayjs instead of moment).
2. Use Manual Chunks Strategically
Split large vendor libraries into separate chunks so they can be cached independently of your application code:
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react')) return 'react-vendor';
if (id.includes('lodash')) return 'lodash-vendor';
return 'vendor';
}
}
}
}
}
3. Lazy Load Routes and Heavy Components
Use dynamic imports for routes, modals, charts, and any component that is not immediately visible. This keeps your initial bundle small and improves time-to-interactive.
4. Optimize Images
Use vite-plugin-imagemin or pre-optimize images before importing them. Consider using modern formats like WebP or AVIF, and use the <picture> element for responsive images.
5. Enable Compression
Generate compressed assets at build time so your server can serve them without on-the-fly compression:
import { vitePluginCompression } from 'vite-plugin-compression';
export default defineConfig({
plugins: [
vitePluginCompression({ algorithm: 'gzip' }),
vitePluginCompression({ algorithm: 'brotliCompress' })
]
});
6. Use Environment Modes Properly
Vite has three default modes: development, production, and test. Use mode-specific .env files to manage configuration across environments. You can also define custom modes:
vite build --mode staging
This loads variables from .env.staging.
7. Profile Your Build
If your build is slow, use the --debug flag or the rollup-plugin-visualizer plugin to identify bottlenecks:
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({ open: true, filename: 'bundle-stats.html' })
]
});
8. Lock Your Vite Version
Vite moves fast, and breaking changes can appear in minor versions. Use a lockfile and test upgrades carefully before deploying to production.
Deployment
After running npm run build, the dist directory contains everything you need. Deploy it to any static host: Netlify, Vercel, GitHub Pages, Cloudflare Pages, S3, or Nginx.
For SPA routing, configure your host to redirect all routes to index.html. For example, in an _redirects file for Netlify:
/* /index.html 200
For Nginx:
location / {
try_files $uri $uri/ /index.html;
}
If you deployed to a subdirectory, remember to set the base option in your Vite config to match.
Conclusion
Vite has fundamentally changed how frontend developers approach build tooling. By leveraging native ES modules in development and Rollup in production, it delivers a developer experience that is both fast and production-ready. In this tutorial, you learned what Vite is, why it matters, how to scaffold and configure projects, work with frameworks and CSS, write plugins, handle SSR and library builds, and apply best practices for performance and deployment. The best way to internalize this knowledge is to build something real — start a side project, migrate an existing Webpack app, or contribute a plugin to the ecosystem. As you spend more time with Vite, you will discover that its thoughtful defaults and extensible architecture make it a tool that grows with you from your first npm create vite command all the way to large-scale production applications.