Vite Complete Guide | Fast Dev Server, Build Optimization, Plugins & Library Mode

Vite Complete Guide | Fast Dev Server, Build Optimization, Plugins & Library Mode

이 글의 핵심

Vite starts a dev server in under 300ms and uses native ESM to avoid bundling during development — resulting in near-instant Hot Module Replacement regardless of project size. This guide covers everything from basic setup to plugin authoring and library mode.

Why Vite?

webpack dev server startup (large project):  15-60 seconds
Vite dev server startup:                    ~300ms

webpack HMR (large project):               2-10 seconds
Vite HMR:                                 <100ms (often <50ms)

Vite achieves this by eliminating the bundling step during development:

webpack:  source → bundle all modules → serve bundle → browser runs bundle
Vite:     source → serve files directly → browser imports via native ESM
          (browser only loads what it actually needs)

Quick Start

# React + TypeScript
npm create vite@latest my-app -- --template react-ts
cd my-app && npm install && npm run dev

# Vue + TypeScript
npm create vite@latest my-app -- --template vue-ts

# Vanilla TypeScript
npm create vite@latest my-app -- --template vanilla-ts

Available templates: vanilla, vanilla-ts, vue, vue-ts, react, react-ts, react-swc, react-swc-ts, preact, preact-ts, lit, lit-ts, svelte, svelte-ts, solid, solid-ts


Project Structure

my-app/
├── public/            # Static assets (served as-is, no processing)
│   └── favicon.ico
├── src/
│   ├── main.tsx       # Entry point
│   ├── App.tsx
│   └── assets/        # Imported assets (processed by Vite)
├── index.html         # Entry HTML (not in public/ — Vite processes it)
├── vite.config.ts
├── tsconfig.json
└── package.json
<!-- index.html — Vite processes this -->
<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <!-- Entry point referenced directly — Vite handles bundling -->
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

vite.config.ts

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

export default defineConfig({
  plugins: [react()],

  // Path aliases
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@hooks': path.resolve(__dirname, './src/hooks'),
    },
  },

  // Dev server
  server: {
    port: 3000,
    open: true,                    // Open browser on start

    // Proxy API calls to backend (avoids CORS in dev)
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },

  // Build settings
  build: {
    outDir: 'dist',
    sourcemap: true,               // Source maps for debugging
    minify: 'esbuild',             // Fast minification
    target: 'es2020',              // Browser target

    rollupOptions: {
      output: {
        // Code splitting: split vendor libraries into separate chunk
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
        },
      },
    },
  },

  // CSS
  css: {
    modules: {
      localsConvention: 'camelCase',  // CSS Module class names as camelCase
    },
    preprocessorOptions: {
      scss: {
        additionalData: '@import "@/styles/variables.scss";',  // Global SCSS
      },
    },
  },
});

Environment Variables

Vite uses .env files. Variables must be prefixed with VITE_ to be exposed to the browser:

# .env (committed — default values)
VITE_APP_NAME=MyApp
VITE_API_URL=http://localhost:8000

# .env.local (not committed — overrides .env locally)
VITE_API_URL=http://localhost:3001

# .env.production (used when building for production)
VITE_API_URL=https://api.myapp.com

# .env.development (used during dev server)
VITE_DEBUG=true
// Access in your code
const apiUrl = import.meta.env.VITE_API_URL;
const appName = import.meta.env.VITE_APP_NAME;

// Built-in Vite env vars
const isDev = import.meta.env.DEV;         // true in dev
const isProd = import.meta.env.PROD;       // true in production
const mode = import.meta.env.MODE;         // 'development' | 'production'
// TypeScript: add types for your env vars
// src/vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_NAME: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

Asset Handling

// Import images — Vite returns the URL
import logoUrl from './logo.svg';
import heroUrl from './hero.png';

function App() {
  return <img src={logoUrl} alt="Logo" />;
}

// Import as raw string
import svgRaw from './icon.svg?raw';
document.body.innerHTML = svgRaw;

// Import as URL explicitly
import fileUrl from './data.json?url';

// Import as inline base64 (small files)
import inlined from './small-icon.png?inline';

// Dynamic import (code splitting)
const { default: Chart } = await import('./Chart');
// vite.config.ts — configure asset handling
export default defineConfig({
  build: {
    assetsInlineLimit: 4096,    // Inline assets < 4KB as base64 (default)
  },
});

Hot Module Replacement (HMR)

Vite’s HMR updates only the changed module — not the whole page.

// Opt into HMR in custom modules
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // Called when this module or its deps update
    console.log('Module updated:', newModule);
  });

  import.meta.hot.dispose(() => {
    // Clean up before the module is replaced
    clearInterval(timer);
  });
}

For React, @vitejs/plugin-react uses React Refresh — components update without losing state.


Plugins

Official Plugins

npm install -D @vitejs/plugin-react      # React with Babel transforms
npm install -D @vitejs/plugin-react-swc  # React with SWC (faster)
npm install -D @vitejs/plugin-vue        # Vue 3
npm install -D @vitejs/plugin-legacy     # Polyfills for older browsers
# Auto-import — import React, useState, etc. without explicit import
npm install -D unplugin-auto-import

# SVG as React components
npm install -D vite-plugin-svgr

# Bundle analysis
npm install -D rollup-plugin-visualizer

# PWA support
npm install -D vite-plugin-pwa
// vite.config.ts with plugins
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    react(),

    svgr({
      svgrOptions: { icon: true },
    }),

    visualizer({
      filename: 'dist/stats.html',    // Open after build to see bundle breakdown
      open: true,
      gzipSize: true,
    }),
  ],
});

Writing a Custom Plugin

// plugins/my-plugin.ts
import type { Plugin } from 'vite';

export function myPlugin(): Plugin {
  return {
    name: 'my-plugin',

    // Transform source files
    transform(code, id) {
      if (!id.endsWith('.ts')) return null;

      // Replace __BUILD_TIME__ with actual timestamp
      return code.replace(/__BUILD_TIME__/g, Date.now().toString());
    },

    // Inject HTML
    transformIndexHtml(html) {
      return html.replace(
        '<head>',
        `<head>\n  <meta name="build-time" content="${Date.now()}" />`
      );
    },

    // Handle virtual modules
    resolveId(id) {
      if (id === 'virtual:my-module') return id;
    },

    load(id) {
      if (id === 'virtual:my-module') {
        return `export const message = 'Hello from virtual module!';`;
      }
    },
  };
}

Library Mode

Build a reusable library instead of an app:

// vite.config.ts — library mode
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import dts from 'vite-plugin-dts';

export default defineConfig({
  plugins: [
    react(),
    dts({ include: ['src'] }),    // Generate .d.ts files
  ],

  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyLibrary',
      fileName: (format) => `my-library.${format}.js`,
      formats: ['es', 'cjs'],     // ESM + CommonJS
    },
    rollupOptions: {
      // Exclude peer dependencies from bundle
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
});
// package.json for the library
{
  "name": "my-library",
  "main": "./dist/my-library.cjs.js",
  "module": "./dist/my-library.es.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/my-library.es.js",
      "require": "./dist/my-library.cjs.js"
    }
  }
}

Multi-Page App (MPA)

// vite.config.ts — multiple entry points
export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        admin: resolve(__dirname, 'admin/index.html'),
        login: resolve(__dirname, 'login/index.html'),
      },
    },
  },
});

Migration from webpack

// Common webpack → Vite equivalents

// webpack: require('./image.png')
// Vite:    import imageUrl from './image.png'

// webpack: require.context
// Vite:    import.meta.glob

// Glob import all files matching a pattern
const modules = import.meta.glob('./routes/*.tsx');
// Returns: { './routes/home.tsx': () => import('./routes/home.tsx'), ... }

// Eager loading (synchronous)
const modules = import.meta.glob('./routes/*.tsx', { eager: true });

// webpack: process.env.REACT_APP_*
// Vite:    import.meta.env.VITE_*

// webpack: DefinePlugin
// Vite:    define in vite.config.ts
export default defineConfig({
  define: {
    __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
  },
});

npm run preview — Test Production Build

npm run build     # Build production output to dist/
npm run preview   # Serve the dist/ folder locally on port 4173

Always test the production build before deploying — dev and prod can behave differently (env vars, asset paths, code splitting).


Quick Reference

TaskConfig
Dev server portserver: { port: 3000 }
API proxyserver: { proxy: { '/api': 'http://localhost:8000' } }
Path aliasresolve: { alias: { '@': './src' } }
Source mapsbuild: { sourcemap: true }
Code splittingbuild: { rollupOptions: { output: { manualChunks: {...} } } }
Global SCSScss: { preprocessorOptions: { scss: { additionalData: '...' } } }
Analyze bundlerollup-plugin-visualizer

Related posts: