Skip to content

Vite: Next-Gen Frontend Build Tool Explained

DodaTech Updated 2026-06-22 7 min read

In this tutorial, you'll learn Vite including project setup, HMR, CSS and asset handling, environment variables, build optimization, and migrating from Webpack to Vite for faster development.

Why Vite Matters

Traditional bundlers like Webpack process your entire application before you can see any change. As projects grow, this wait time becomes painful. Vite takes a different approach: it serves source files natively during development and bundles only when you build for production. The result is instant server start and near-instant Hot Module Replacement.

By the end of this guide, you will scaffold a Vite project, configure plugins, optimize builds, and migrate an existing project from Webpack.

What is Vite?

Vite (French for "quick") is a build tool created by Evan You, the creator of Vue.js. It leverages native ES modules in the browser during development and uses Rollup for production builds.

flowchart LR
  A[Vite] --> B[Dev Server]
  A --> C[Build Command]
  B --> D[Native ESM]
  B --> E[HMR]
  B --> F[esbuild Transpile]
  C --> G[Rollup Bundle]
  G --> H[Code Splitting]
  G --> I[Tree Shaking]
  G --> J[Asset Optimization]

Project Setup

# Create a Vite project
npm create vite@latest my-app

# Or specify a template
npm create vite@latest my-app -- --template react-ts

# Navigate and install
cd my-app
npm install

# Start dev server
npm run dev

Expected Output

$ npm run dev

  VITE v6.0.0  ready in 245ms

  Local:   http://localhost:5173/
  Network: use --host to expose

Project Structure

my-app/
├── index.html          # Entry point
├── vite.config.js      # Vite configuration
├── package.json
├── public/             # Static assets
│   └── favicon.ico
├── src/
│   ├── main.jsx        # Application entry
│   ├── App.jsx
│   ├── App.css
│   └── assets/         # Processed assets
│       └── logo.svg
└── node_modules/

Configuration

Vite configuration lives in vite.config.js or vite.config.ts.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: 'dist',
    sourcemap: true,
    minify: 'esbuild',
  },
})

Hot Module Replacement

HMR in Vite updates only the changed module without reloading the page.

// src/counter.js
export function setupCounter(element) {
  let counter = 0
  const setCounter = (count) => {
    counter = count
    element.innerHTML = `count is ${counter}`
  }
  element.addEventListener('click', () => setCounter(counter + 1))
  setCounter(0)
}

When you edit this file, Vite updates only the changed module. The counter state is preserved because HMR does not reload the page.

How HMR Works

sequenceDiagram
  Developer->>Vite: Edit counter.js
  Vite->>Browser: Send WebSocket update
  Browser->>Browser: Re-import counter.js module
  Browser->>Browser: Apply changes without reload
  Note over Browser: Counter state preserved

CSS and Assets

CSS Imports

// src/main.jsx
import './style.css'
import './responsive.css'

CSS Modules

/* src/Button.module.css */
.button {
  padding: 10px 20px;
  background: blue;
  color: white;
}
// src/Button.jsx
import styles from './Button.module.css'

export function Button() {
  return <button className={styles.button}>Click</button>
}

Asset URLs

import logo from './assets/logo.svg'

// logo is the resolved URL
const img = `<img src="${logo}" alt="Logo" />`

Expected Behavior

Assets smaller than 4KB are inlined as base64. Larger files are copied to the dist directory with hashed filenames.

$ npm run build
dist/
├── assets/
│   ├── logo-a1b2c3d4.svg
│   ├── index-c5d6e7f8.js
│   └── index-e9f0a1b2.css
└── index.html

Environment Variables

Vite uses import.meta.env for environment variables. Variables must be prefixed with VITE_ to be exposed to client code.

# .env
VITE_API_URL=http://localhost:8080
VITE_APP_TITLE=My App

# .env.development
VITE_API_URL=http://localhost:8080

# .env.production
VITE_API_URL=https://api.example.com
// src/api.js
const apiUrl = import.meta.env.VITE_API_URL

export async function fetchData() {
  const response = await fetch(`${apiUrl}/data`)
  return response.json()
}

Expected Output

console.log(import.meta.env.VITE_APP_TITLE)
// "My App"
console.log(import.meta.env.MODE)
// "development" or "production"

Build Optimization

Code Splitting

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['./src/utils.js'],
        },
      },
    },
  },
})

Tree Shaking

Vite automatically tree-shakes unused exports using Rollup. This removes dead code from the production bundle.

// src/math.js
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b }
export function multiply(a, b) { return a * b }

// src/main.jsx
import { add } from './math'
console.log(add(2, 3))

The subtract and multiply functions are excluded from the production bundle.

Chunk Size Warning

// vite.config.js
export default defineConfig({
  build: {
    chunkSizeWarningLimit: 500, // KB
  },
})

Migrating from Webpack

Before (Webpack)

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' }),
  ],
  devServer: {
    port: 3000,
    hot: true,
  },
}

After (Vite)

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: { port: 3000 },
})

And update index.html:

<!-- public/index.html (before) -->
<!-- No script tag needed -->

<!-- index.html (after) -->
<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>
</html>

Migration Steps

  1. Remove Webpack config, babel config, and related npm packages
  2. Move index.html to project root
  3. Add Vite config with necessary plugins
  4. Update package.json scripts: "dev": "vite", "build": "vite build"
  5. Replace process.env with import.meta.env
  6. Run npm run dev and fix any import issues

Common Errors

Problem Cause Fix
import.meta.env undefined Not using Vite's dev server Use vite or vite build commands
Images not showing in production Wrong asset path Use import or new URL('./asset.png', import.meta.url)
HMR not working File not properly imported Ensure the file is imported via import, not a direct script tag
CORS errors in dev API on different port Configure server.proxy in vite.config.js
Build fails with syntax error Missing plugin for JSX/TypeScript Add @vitejs/plugin-react or @vitejs/plugin-vue

Practice Questions

1. What technology does Vite use for development-time code transformation?

esbuild.

2. How do you access environment variables in Vite?

import.meta.env.VITE_VARIABLE_NAME.

3. What is the purpose of manualChunks in Vite's build configuration?

To manually control Code Splitting, grouping modules into separate chunks.

4. How does Vite's dev server serve JavaScript differently from Webpack?

Vite serves native ES modules without Bundling. Webpack bundles everything before serving.

5. What is the index.html role in a Vite project?

It is the entry point that references the application's source files via <script type="module">.

Challenge

Migrate a small React application from Create React App to Vite. The application must use environment variables for API configuration, CSS modules for styling, and include at least one dynamic import. Compare the dev server start time and build output size between CRA and Vite.

Real-World Task

Set up a new TypeScript + React project with Vite. Configure proxy for an API server, set up environment variables for development and production, implement Code Splitting for route-based chunks, and configure the build to output to a custom directory. Deploy the production build to a static hosting platform.

Is Vite only for frontend frameworks?

No. Vite works with vanilla JavaScript, TypeScript, and any frontend framework. There are templates for vanilla, Vue, React, Svelte, Preact, Lit, and Solid.

How does Vite compare to Next.js?

Next.js is a full framework with server-side rendering, static generation, and API routes. Vite is a build tool and dev server. They serve different purposes.

Can I use Vite with backend projects?

Vite is primarily for frontend development. However, you can use vite build to bundle backend TypeScript code by configuring the build target to Node.js.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro