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: The Next-Generation Frontend Tooling

Deep dive into Vite 5: Rolldown, Environment API, new features, and migration from Webpack.

ViteFrontendBuild ToolsJavaScript

By MinhVo

Introduction

Since its initial release in 2020, Vite has evolved from an exciting experiment into the standard build tool for modern frontend development. By 2024, Vite 5 had matured significantlyβ€”offering a robust plugin system, production-grade SSR support, framework-specific integrations, and a growing ecosystem of community plugins. What started as Evan You's side project now powers Vue, SvelteKit, Astro, and countless production applications.

This guide focuses on the practical aspects of using Vite in production: building plugins, implementing server-side rendering, optimizing builds, configuring for monorepos, and migrating from Webpack. If you've already experienced Vite's lightning-fast dev server and want to leverage its full power, this is the deep dive you need.

Frontend tooling evolution

Understanding Vite: Core Concepts

Vite's Architecture

Vite uses a dual architecture: esbuild-powered dependency pre-bundling and native ESM dev server for development, and Rollup-based production builds. Understanding this split is essential for advanced usage.

// Development flow:
// Browser β†’ native ESM request β†’ Vite dev server β†’ esbuild transform β†’ response
// Dependencies β†’ pre-bundled once by esbuild β†’ cached in node_modules/.vite
 
// Production flow:
// Entry point β†’ Rollup bundling β†’ tree-shaking β†’ code splitting β†’ optimized output

The Plugin Interface

Vite plugins implement hooks from both Vite's own API and Rollup's plugin interface. This dual compatibility means most Rollup plugins work with Vite out of the box, while Vite-specific hooks provide access to the dev server, HMR system, and HTML transformation.

interface Plugin {
  name: string;
 
  // Vite-specific hooks
  configureServer(server: ViteDevServer): void;
  transformIndexHtml(html: string, ctx: IndexHtmlTransformContext): string;
  handleHotUpdate(ctx: HmrContext): void;
 
  // Rollup-compatible hooks
  options(options: InputOptions): InputOptions;
  buildStart(options: InputOptions): void;
  resolveId(source: string, importer: string): string | null;
  load(id: string): string | null;
  transform(code: string, id: string): TransformResult;
  generateBundle(options: OutputOptions, bundle: OutputBundle): void;
}

HMR API Deep Dive

Vite's HMR API allows fine-grained control over how modules are updated in the browser:

if (import.meta.hot) {
  // Accept self-updates
  import.meta.hot.accept((newModule) => {
    // Update the DOM or re-render
  });
 
  // Accept updates from dependencies
  import.meta.hot.accept('./dependency', (newDep) => {
    // Handle updated dependency
  });
 
  // Decline updates β€” forces full page reload
  import.meta.hot.decline();
 
  // Listen for custom events
  import.meta.hot.on('custom-event', (data) => {
    console.log('Custom HMR event:', data);
  });
 
  // Clean up side effects when module is replaced
  import.meta.hot.dispose((data) => {
    data.cleanup = myCleanupFunction;
    clearInterval(myInterval);
  });
}

Plugin architecture

Implementation Guide

Building Custom Plugins

Environment-Aware Configuration Plugin

import { Plugin } from 'vite';
 
function envPlugin(): Plugin {
  let isDev: boolean;
 
  return {
    name: 'vite-plugin-env',
 
    config(config, { command }) {
      isDev = command === 'serve';
 
      return {
        define: {
          __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
          __BUILD_TIME__: JSON.stringify(new Date().toISOString()),
          __IS_DEV__: isDev,
        },
      };
    },
 
    transformIndexHtml(html) {
      if (!isDev) {
        return html.replace(
          '<head>',
          `<head>
            <meta name="version" content="${process.env.npm_package_version}">`
        );
      }
    },
  };
}

Virtual Modules Plugin

Virtual modules are modules that don't exist on disk but are generated by plugins:

import { Plugin } from 'vite';
import glob from 'fast-glob';
 
function virtualRoutesPlugin(): Plugin {
  const virtualModuleId = 'virtual:routes';
  const resolvedVirtualModuleId = '\0' + virtualModuleId;
 
  return {
    name: 'vite-plugin-virtual-routes',
 
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId;
      }
    },
 
    load(id) {
      if (id === resolvedVirtualModuleId) {
        const pages = glob.sync('src/pages/**/*.tsx');
        const routes = pages.map((page) => {
          const path = page
            .replace('src/pages', '')
            .replace(/\.tsx$/, '')
            .replace(/\/index$/, '/');
 
          return `{
            path: '${path}',
            component: () => import('./${page}')
          }`;
        });
 
        return `export default [${routes.join(',\n')}]`;
      }
    },
  };
}

Markdown Transform Plugin

import { Plugin } from 'vite';
import matter from 'gray-matter';
import { marked } from 'marked';
 
function markdownPlugin(): Plugin {
  return {
    name: 'vite-plugin-markdown',
 
    transform(code, id) {
      if (!id.endsWith('.md')) return;
 
      const { data, content } = matter(code);
      const html = marked(content);
 
      return {
        code: `
          export const frontmatter = ${JSON.stringify(data)};
          export const html = ${JSON.stringify(html)};
          export default ${JSON.stringify(html)};
        `,
        map: null,
      };
    },
  };
}

Server-Side Rendering (SSR)

Vite provides built-in SSR support through ssrLoadModule, which transforms and executes modules in Node.js:

import express from 'express';
import { createServer as createViteServer } from 'vite';
 
async function createServer() {
  const isProd = process.env.NODE_ENV === 'production';
  const app = express();
 
  let vite;
 
  if (!isProd) {
    vite = await createViteServer({
      server: { middlewareMode: true },
      appType: 'custom',
    });
    app.use(vite.middlewares);
  } else {
    app.use(express.static('dist/client'));
  }
 
  app.use('*', async (req, res) => {
    const url = req.originalUrl;
 
    try {
      let template, render;
 
      if (!isProd && vite) {
        template = await readFile('index.html', 'utf-8');
        template = await vite.transformIndexHtml(url, template);
        render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render;
      } else {
        template = await readFile('dist/client/index.html', 'utf-8');
        render = (await import('./dist/server/entry-server.js')).render;
      }
 
      const appHtml = render(url);
      const html = template.replace('<!--ssr-outlet-->', appHtml);
 
      res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
    } catch (e) {
      if (vite) vite.ssrFixStacktrace(e);
      console.error(e);
      res.status(500).end(e.message);
    }
  });
 
  app.listen(3000);
}
 
createServer();

SSR Entry Points

// src/entry-server.tsx
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import App from './App';
 
export function render(url: string) {
  return renderToString(
    <StaticRouter location={url}>
      <App />
    </StaticRouter>
  );
}
 
// src/entry-client.tsx
import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
 
hydrateRoot(
  document.getElementById('root')!,
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

Step-by-Step Implementation

Monorepo Configuration

// packages/shared/vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'path';
 
export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      formats: ['es', 'cjs'],
      fileName: 'index',
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
    },
  },
});
 
// apps/web/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
 
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@shared': resolve(__dirname, '../../packages/shared/src'),
    },
  },
  server: {
    watch: {
      ignored: ['!**/packages/shared/**'],
    },
  },
});

Webpack Migration Guide

// Step 1: Install Vite
// npm i -D vite @vitejs/plugin-react
 
// Step 2: Create vite.config.ts
// Map webpack.config.js settings to Vite equivalents:
// babel-loader β†’ @vitejs/plugin-react
// css-loader/style-loader β†’ built-in
// sass-loader β†’ sass package (auto-detected)
// ts-loader β†’ built-in (esbuild)
 
// Step 3: Update entry point
// Webpack: entry: './src/index.js'
// Vite: <script type="module" src="/src/main.tsx"></script> in index.html
 
// Step 4: Replace environment variables
// Webpack: process.env.REACT_APP_*
// Vite: import.meta.env.VITE_*
 
// Step 5: Update path aliases
// Webpack: resolve.alias in config
// Vite: resolve.alias in vite.config.ts + tsconfig paths

Advanced Build Configuration

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
 
export default defineConfig({
  plugins: [
    react(),
    visualizer({ open: true, gzipSize: true, brotliSize: true }),
  ],
 
  build: {
    target: 'es2020',
    cssCodeSplit: true,
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react-dom')) return 'vendor-react';
            if (id.includes('lodash') || id.includes('date-fns')) return 'vendor-utils';
            return 'vendor';
          }
        },
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash].[ext]',
      },
    },
    sourcemap: true,
    chunkSizeWarningLimit: 500,
  },
});

Build optimization

Use Cases and Real-World Applications

Component Library with HMR

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
 
export default defineConfig({
  plugins: [react(), dts({ include: ['src'] })],
  build: {
    lib: {
      entry: 'src/index.ts',
      formats: ['es', 'cjs'],
      fileName: 'index',
    },
    rollupOptions: {
      external: ['react', 'react-dom', 'react/jsx-runtime'],
    },
  },
});

Micro-Frontend with Module Federation

import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';
 
export default defineConfig({
  plugins: [
    federation({
      name: 'host',
      remotes: {
        remote_app: 'http://localhost:3001/assets/remoteEntry.js',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
});

Internationalization Plugin

import { Plugin } from 'vite';
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
 
function i18nPlugin(localesDir: string): Plugin {
  return {
    name: 'vite-plugin-i18n',
 
    resolveId(id) {
      if (id === 'virtual:i18n') return '\0virtual:i18n';
    },
 
    load(id) {
      if (id !== '\0virtual:i18n') return;
 
      const locales = {};
      const files = ['en', 'es', 'fr', 'de', 'ja'];
 
      for (const lang of files) {
        const path = resolve(localesDir, `${lang}.json`);
        if (existsSync(path)) {
          locales[lang] = JSON.parse(readFileSync(path, 'utf-8'));
        }
      }
 
      return `
        const locales = ${JSON.stringify(locales)};
        let currentLocale = 'en';
        export function setLocale(locale) { currentLocale = locale; }
        export function t(key, params = {}) {
          let text = locales[currentLocale]?.[key] || key;
          for (const [k, v] of Object.entries(params)) {
            text = text.replace(new RegExp('\\\\{' + k + '\\\\}', 'g'), v);
          }
          return text;
        }
        export default { setLocale, t, locales };
      `;
    },
  };
}

Best Practices

  1. Use optimizeDeps.include for heavy dependencies β€” Pre-bundle large CJS dependencies to avoid slow page loads in development.

  2. Implement proper SSR error handling β€” Always wrap ssrLoadModule calls in try-catch and use ssrFixStacktrace for meaningful error messages.

  3. Use CSS Modules over global CSS β€” CSS Modules provide scoping, work with HMR, and have zero runtime overhead in Vite.

  4. Configure build.rollupOptions.output.manualChunks β€” Split vendor code into separate chunks for better caching strategies.

  5. Use vite-plugin-checker for type checking β€” Run TypeScript type checking and ESLint in a separate thread to avoid blocking the dev server.

  6. Enable css.devSourcemap in development β€” CSS source maps make debugging styles significantly easier.

  7. Use server.proxy for API development β€” Avoid CORS issues by proxying API requests to your backend server.

  8. Test production builds with vite preview β€” Always verify the production build locally before deploying.

Common Pitfalls and Solutions

PitfallImpactSolution
CJS dependencies causing slow dev startup5-10s delay on first page loadAdd to optimizeDeps.include
SSR hydration mismatchesConsole warnings, visual glitchesEnsure server and client render identical HTML
Missing external in library buildBundled peer dependenciesExternalize react, react-dom, etc.
CSS not loading in SSRFlash of unstyled contentUse vite.ssrLoadModule for CSS handling
HMR not working for custom file typesFull page reload on changeImplement handleHotUpdate in plugin
Path aliases not working in TypeScriptType errors despite runtime workingUpdate tsconfig.json paths to match

Performance Optimization

Development Performance

export default defineConfig({
  optimizeDeps: {
    include: ['react', 'react-dom', 'react-router-dom', 'axios', 'lodash-es'],
    exclude: ['@myorg/shared'],
  },
  server: {
    watch: { usePolling: false },
    warmup: {
      clientFiles: ['./src/components/**/*.tsx', './src/pages/**/*.tsx'],
    },
  },
});

Production Build Optimization

export default defineConfig({
  build: {
    minify: 'esbuild',
    target: 'es2020',
    cssCodeSplit: true,
    reportCompressedSize: true,
    rollupOptions: {
      treeshake: {
        moduleSideEffects: false,
        propertyReadSideEffects: false,
      },
    },
  },
});

Comparison with Alternatives

FeatureVite 5Webpack 5Parcel 2esbuild (standalone)
Dev startup~300ms~5s~1s~100ms
HMR~30ms~500ms~100msN/A
Production buildRollupWebpackesbuildesbuild
SSR supportBuilt-inManualLimitedManual
Plugin systemRollup compat + customLoaders + pluginsTransformersLimited
CSS handlingBuilt-in + PostCSSLoadersBuilt-inLimited
Tree shakingExcellent (Rollup)GoodGoodGood
ConfigurationMinimalExtensiveZeroMinimal

Advanced Patterns

Custom Dev Server Middleware

import { Plugin } from 'vite';
 
function apiMockPlugin(): Plugin {
  return {
    name: 'api-mock',
    configureServer(server) {
      server.middlewares.use('/api', (req, res, next) => {
        if (req.url === '/api/users') {
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify([
            { id: 1, name: 'Alice' },
            { id: 2, name: 'Bob' },
          ]));
          return;
        }
        next();
      });
    },
  };
}

Build-Time Data Generation

import { Plugin } from 'vite';
import { execSync } from 'child_process';
 
function buildInfoPlugin(): Plugin {
  return {
    name: 'build-info',
    config() {
      const gitHash = execSync('git rev-parse --short HEAD').toString().trim();
      const gitBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
 
      return {
        define: {
          __GIT_HASH__: JSON.stringify(gitHash),
          __GIT_BRANCH__: JSON.stringify(gitBranch),
          __BUILD_TIME__: JSON.stringify(new Date().toISOString()),
        },
      };
    },
  };
}

Testing with Vitest

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
 
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/test/setup.ts',
    coverage: {
      provider: 'v8',
      include: ['src/**/*.{ts,tsx}'],
      exclude: ['src/**/*.test.{ts,tsx}', 'src/**/*.d.ts'],
    },
  },
});

Future Outlook

Vite's roadmap is ambitious. Rolldown, a Rust-based bundler, is being developed to replace both Rollup and esbuild within Vite, promising 10-30x faster builds. The Environment API (Vite 5) enables multi-environment builds targeting browser, Node.js, and edge from a single configuration.

The ecosystem continues to grow rapidly. Framework authors are standardizing on Vite, and the plugin ecosystem is expanding with solutions for every use case. The future of frontend tooling is fast, and Vite is leading the way.

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 has matured from a promising experiment into the standard build tool for modern frontend development. Its plugin system, SSR support, and build optimization capabilities make it suitable for everything from simple SPAs to complex full-stack applications.

Key takeaways:

  1. The plugin system is powerful and Rollup-compatible β€” leverage existing Rollup plugins and build custom ones
  2. SSR is built-in β€” use ssrLoadModule for development and pre-built SSR bundles for production
  3. HMR is fast and fine-grained β€” use the import.meta.hot API for custom update behavior
  4. Monorepo support is excellent β€” configure aliases and watch settings for linked packages
  5. Migration from Webpack is straightforward β€” most patterns have direct Vite equivalents
  6. Production builds are optimized β€” Rollup handles tree-shaking, code splitting, and minification
  7. The ecosystem is thriving β€” official plugins for React, Vue, Svelte, and a growing community

Start with a new Vite project to experience the difference, then migrate existing Webpack projects one at a time. The developer experience improvement is transformative.