MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr πŸ”₯ tagline

Hey there πŸ‘‹ I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 β€” present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 β€” Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms β€” earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Vite: Next-Generation Frontend Tooling

Master Vite: instant dev server, optimized builds, plugin ecosystem, and framework-agnostic tooling.

ViteFrontendBuild ToolsJavaScript

By MinhVo

Introduction

Frontend build tools have been a source of frustration for years. Webpack, while powerful, is notoriously slowβ€”starting a dev server on a large project can take 30+ seconds, and Hot Module Replacement (HMR) updates degrade as the project grows. Enter Vite (French for "fast"), created by Evan You (creator of Vue.js). Vite reimagines the development experience by leveraging native ES modules in the browser, eliminating the need to bundle your entire application during development.

The result is staggering: dev server startup in milliseconds regardless of project size, instant HMR that stays fast as your codebase grows, and optimized production builds powered by Rollup. Vite isn't just fasterβ€”it fundamentally changes the relationship between your source code and the browser, making development feel like editing static HTML again while providing all the features modern developers expect.

Since its release in 2020, Vite has been adopted by Vue, React, Svelte, Preact, and Lit communities. Frameworks like SvelteKit, Nuxt 3, and Astro have chosen Vite as their build foundation. This guide explores why Vite is so fast, how to set it up, and the patterns that make it superior to traditional bundlers for development.

Frontend build tools

Understanding Vite: Core Concepts

The Bundling Problem

Traditional bundlers like Webpack and Parcel parse your entire application's dependency graph and bundle everything into one or more output files before serving them. This approach works well for production (optimized, tree-shaken bundles) but creates a terrible development experience:

// With Webpack, this simple app requires bundling ALL dependencies:
import React from 'react';          // ~40KB
import ReactDOM from 'react-dom';   // ~120KB
import { BrowserRouter } from 'react-router-dom'; // ~30KB
import App from './App';            // Your code
import './styles.css';              // Processed CSS
 
// Webpack must:
// 1. Resolve all imports recursively
// 2. Parse and transform every file
// 3. Bundle everything into one file
// 4. Serve the bundle
// This takes 10-60 seconds on large projects!

How Vite Solves This

Vite splits the problem into two parts. During development, it uses native ES modulesβ€”the browser already knows how to import modules, so Vite just serves them. When you request http://localhost:3000, Vite serves your index.html with a <script type="module"> tag. The browser then makes individual requests for each import, and Vite transforms and serves each file on demand.

For production, Vite uses Rollup for optimized bundling. Rollup produces smaller, more efficient bundles with proper tree-shaking, code splitting, and chunk optimization.

// Browser requests during development:
// GET / β†’ index.html
// GET /src/main.tsx β†’ Vite transforms TypeScript β†’ serves JS
// GET /src/App.tsx β†’ Vite transforms TSX β†’ serves JSX
// GET /node_modules/react/index.js β†’ Vite serves directly
// Only the files actually used are processed!

Native ES Modules

The key technology enabling Vite is browser-native ES modules. Modern browsers support <script type="module">, which allows direct import/export syntax. The browser fetches each module, parses its imports, and fetches those tooβ€”each as a separate HTTP request. Vite intercepts these requests and transforms the files (TypeScript to JavaScript, JSX to function calls, etc.) before serving them.

Dependency Pre-Bundling

One challenge with native ES modules: a package like lodash-es has hundreds of small modules. Loading each one separately would create hundreds of HTTP requests. Vite solves this with dependency pre-bundling using esbuild (written in Go, 10-100x faster than JavaScript-based bundlers):

// Before pre-bundling: 600+ requests for lodash-es
import { debounce } from 'lodash-es';
// β†’ /node_modules/lodash-es/debounce.js
// β†’ /node_modules/lodash-es/_root.js
// β†’ ... hundreds more
 
// After pre-bundling: 1 request
import { debounce } from 'lodash-es';
// β†’ /node_modules/.vite/lodash-es.js (single bundled file)

This happens automatically on first run and is cached until your package.json changes.

Vite architecture

Architecture and Design Patterns

Vite's Development Server Architecture

The dev server consists of several components working together. The module transformer uses esbuild to convert TypeScript, JSX, and TSX into plain JavaScript on the fly. The CSS processor handles PostCSS, SCSS, and CSS Modules natively. The dependency pre-bundler uses esbuild to bundle node_modules into single files. The HMR engine watches for file changes and sends updates via WebSocket to the browser.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Browser                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ index.html  β”‚  β”‚ main.tsx     β”‚  β”‚ App.tsx       β”‚  β”‚
β”‚  β”‚ (entry)     β”‚β†’ β”‚ (transformed)β”‚β†’ β”‚ (transformed) β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ HTTP requests (native ESM)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Vite Dev Server                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Module       β”‚  β”‚ CSS          β”‚  β”‚ Dependency   β”‚  β”‚
β”‚  β”‚ Transformer  β”‚  β”‚ Processor    β”‚  β”‚ Pre-Bundler  β”‚  β”‚
β”‚  β”‚ (esbuild)    β”‚  β”‚ (PostCSS)    β”‚  β”‚ (esbuild)    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚  β”‚ HMR Engine   β”‚  β”‚ WebSocket    β”‚                     β”‚
β”‚  β”‚ (chokidar)   β”‚  β”‚ Server       β”‚                     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Hot Module Replacement (HMR)

Vite's HMR is significantly faster than Webpack's because it only needs to transform the changed module, not rebundle the entire application. When you change src/App.tsx, chokidar detects the file change, Vite transforms only that file using esbuild, sends the HMR update via WebSocket, and the browser replaces only the changed module. HMR updates happen in under 50ms regardless of project size.

Plugin System

Vite's plugin system extends both the dev server and build pipeline. Plugins are compatible with Rollup's plugin interface, plus Vite-specific hooks:

import { defineConfig, Plugin } from 'vite';
 
function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
 
    // Vite-specific hooks
    configureServer(server) {
      server.middlewares.use('/api', (req, res) => {
        res.json({ message: 'Custom API endpoint' });
      });
    },
 
    handleHotUpdate({ file, server }) {
      if (file.endsWith('.mdx')) {
        server.ws.send({
          type: 'custom',
          event: 'mdx-update',
          data: { file },
        });
        return [];
      }
    },
 
    // Rollup-compatible hooks
    transform(code, id) {
      if (id.endsWith('.custom')) {
        return transformCustomFormat(code);
      }
    },
  };
}
 
export default defineConfig({
  plugins: [myPlugin()],
});

Step-by-Step Implementation

Creating a Vite Project

# Create a new project with React
npm create vite@latest my-app -- --template react-ts
 
# Or with Vue
npm create vite@latest my-app -- --template vue-ts
 
# Install dependencies and start
cd my-app
npm install
npm run dev
# Server starts in <300ms!

Configuration Deep Dive

// 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,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
 
  build: {
    outDir: 'dist',
    sourcemap: true,
    minify: 'esbuild',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
      },
    },
  },
 
  css: {
    modules: {
      localsConvention: 'camelCase',
    },
    preprocessorOptions: {
      scss: {
        additionalData: `@import "./src/styles/variables.scss";`,
      },
    },
  },
 
  resolve: {
    alias: {
      '@': '/src',
      '@components': '/src/components',
    },
  },
});

TypeScript Integration

Vite handles TypeScript natively using esbuildβ€”no tsc needed for development:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"]
}

CSS and Asset Handling

// Import CSS β€” auto-injected into <style> tag in dev
import './styles/main.css';
 
// CSS Modules
import styles from './Card.module.css';
 
// Import assets β€” returns URL string
import logo from './assets/logo.png';
 
// Dynamic import β€” code splitting
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

Vite performance

Use Cases and Real-World Applications

React SPA Migration from CRA

Migrating a Create React App project to Vite involves installing Vite and the React plugin, creating a vite.config.ts, moving index.html to the project root with a <script type="module"> tag, replacing process.env with import.meta.env, and updating package.json scripts. The result: dev startup drops from ~25s to ~300ms, and HMR goes from ~2s to ~30ms.

Vue 3 Library Development

Vite's library mode builds distributable packages with proper external dependencies:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
 
export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyLib',
      formats: ['es', 'cjs', 'umd'],
      fileName: (format) => `my-lib.${format}.js`,
    },
    rollupOptions: {
      external: ['vue'],
      output: { globals: { vue: 'Vue' } },
    },
  },
});

Environment Variables and Modes

// .env
VITE_APP_TITLE=My Application
VITE_API_URL=https://api.example.com
 
// Access in code (only VITE_ prefixed vars are exposed)
console.log(import.meta.env.VITE_APP_TITLE);
console.log(import.meta.env.MODE);
console.log(import.meta.env.PROD);

Best Practices

  1. Use import.meta.env instead of process.env β€” Vite exposes environment variables through import.meta.env. Only variables prefixed with VITE_ are exposed to client code.

  2. Leverage dependency pre-bundling β€” Add large CommonJS dependencies to optimizeDeps.include to pre-bundle them on first run rather than on page load.

  3. Use CSS Modules for component styles β€” CSS Modules provide scoping without runtime overhead. Vite supports them natively with .module.css extension.

  4. Configure path aliases β€” Use resolve.alias for clean imports (@/components/Button instead of ../../../components/Button).

  5. Enable build sourcemaps for debugging β€” Set build.sourcemap: true for production builds. The sourcemap cost is minimal compared to debugging benefits.

  6. Use code splitting wisely β€” Let Vite's automatic code splitting work, but use manualChunks for vendor libraries to improve caching.

  7. Proxy API requests in development β€” Use server.proxy to forward API requests to your backend, avoiding CORS issues.

  8. Use vite preview to test production builds β€” Before deploying, run vite preview to serve the production build locally and verify everything works.

Common Pitfalls and Solutions

PitfallImpactSolution
Using process.env instead of import.meta.envUndefined values at runtimeUse import.meta.env.VITE_*
CJS dependencies not pre-bundledSlow initial page loadAdd to optimizeDeps.include
Missing VITE_ prefix on env varsVariables not exposed to clientPrefix with VITE_
Hardcoded paths in importsBreaks on buildUse resolve.alias
CSS specificity issuesStyles not appliedUse CSS Modules
Missing isolatedModules in tsconfigTypeScript errorsSet isolatedModules: true
Assuming index.html is in public/Entry point not foundIn Vite, index.html is at project root

Performance Optimization

export default defineConfig({
  optimizeDeps: {
    include: ['react', 'react-dom', 'react-router-dom', 'lodash-es'],
    exclude: ['your-local-package'],
  },
  server: {
    watch: {
      usePolling: false,
      interval: 100,
    },
  },
  build: {
    minify: 'esbuild',
    target: 'es2020',
    chunkSizeWarningLimit: 500,
    cssCodeSplit: true,
  },
});

Vite vs Webpack Benchmarks

MetricWebpack 5Vite 2Improvement
Cold start (small project)3s200ms15x
Cold start (large project)30s300ms100x
HMR update500ms-2s20-50ms20-40x
Production build45s15s3x

Comparison with Alternatives

FeatureViteWebpack 5Parcel 2Snowpack
Dev startup~200ms~5s~1s~300ms
HMR speed~30ms~500ms~100ms~50ms
Production bundlerRollupWebpackParcelNot included
ConfigurationMinimalExtensiveZeroMinimal
TypeScriptNative (esbuild)LoaderNativeNative
Framework supportAll majorAll majorAll majorLimited

Advanced Patterns

Custom Plugin for SVG Components

import { Plugin } from 'vite';
import { readFileSync } from 'fs';
 
function svgComponentPlugin(): Plugin {
  return {
    name: 'svg-component',
    transform(code, id) {
      if (!id.endsWith('.svg?component')) return;
 
      const svg = readFileSync(id.replace('?component', ''), 'utf-8');
      return `
        const svg = \`${svg}\`;
        export default function SvgComponent({ className }) {
          const div = document.createElement('div');
          div.innerHTML = svg;
          const el = div.firstElementChild;
          if (className) el.setAttribute('class', className);
          return el;
        }
      `;
    },
  };
}

SSR Setup

import express from 'express';
import { createServer as createViteServer } from 'vite';
 
async function createServer() {
  const app = express();
  const vite = await createViteServer({
    server: { middlewareMode: true },
  });
 
  app.use(vite.middlewares);
 
  app.use('*', async (req, res) => {
    const { render } = await vite.ssrLoadModule('/src/entry-server.ts');
    const html = await render(req.originalUrl);
    const template = await vite.transformIndexHtml(req.originalUrl, html);
    res.status(200).set({ 'Content-Type': 'text/html' }).end(template);
  });
 
  app.listen(3000);
}
 
createServer();

Testing with Vitest

// vitest.config.ts
import { defineConfig } from 'vitest/config';
 
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/test/setup.ts',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
});
 
// Example test
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import App from './App';
 
describe('App', () => {
  it('renders the title', () => {
    render(<App />);
    expect(screen.getByText('Hello Vite')).toBeDefined();
  });
});

Community Resources and Further Learning

The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.

Curated Learning Pathways

Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.

Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.

Contributing to Open Source

Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.

# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
 
# Run the project's contribution setup
npm run setup:dev
npm run test  # Ensure tests pass before making changes
 
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
 
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
 
Closes #1234"
git push origin fix/issue-description

Building a Technical Knowledge Base

Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.

Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.

Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.

Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.

Mentorship and Knowledge Sharing

Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.

Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.

Conclusion

Vite represents a paradigm shift in frontend tooling. By leveraging native ES modules for development and Rollup for production, it delivers an unmatched combination of speed and correctness.

Key takeaways:

  1. Native ES modules eliminate bundling in development β€” startup is instant regardless of project size
  2. esbuild pre-bundles dependencies β€” 10-100x faster than JavaScript-based bundlers
  3. HMR stays fast β€” only the changed module is transformed, not the entire app
  4. Rollup handles production β€” optimized, tree-shaken bundles with code splitting
  5. Configuration is minimal β€” sensible defaults, no loader configuration needed
  6. Framework-agnostic β€” works with React, Vue, Svelte, Preact, Lit, and more
  7. The plugin system is Rollup-compatible β€” leverage the existing Rollup ecosystem

If you're still using Webpack for development, switching to Vite is the single highest-impact improvement you can make to your development workflow. The time saved waiting for builds compounds across every developer, every day, every save.