← Back to DevBytes

Vite from Beginner to Expert: A Learning Path

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:

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:

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

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

Useful Community Plugins

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:

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles