All topics
Frontend · Learning hub

Vite notes for developers

Master Vite with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
Vite

Vite Configuration & Features

Vite Configuration & Features Why Vite? Dev server uses native ES modules — instant server start (no bundling needed) HMR (Hot Module Replacement) is blazing fa

Vite Configuration & Features

Why Vite?

  • Dev server uses native ES modules — instant server start (no bundling needed)

  • HMR (Hot Module Replacement) is blazing fast — updates only changed module

  • Production build uses Rollup — optimized, tree-shaken output

  • Pre-bundles dependencies with esbuild (100x faster than JS-based bundlers)

vite.config.ts

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

export default defineConfig({
  plugins: [
    react(),                        // React fast refresh
    // @vitejs/plugin-vue for Vue
    // @vitejs/plugin-svelte for Svelte
  ],

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

  server: {
    port: 3000,
    open: true,                     // auto-open browser
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },

  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
        },
      },
    },
    chunkSizeWarningLimit: 1000,    // KB
  },

  css: {
    modules: {
      localsConvention: 'camelCase',
    },
    preprocessorOptions: {
      scss: {
        additionalData: '@import "@/styles/variables.scss";',
      },
    },
  },

  test: {                           // Vitest config (co-located)
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      reporter: ['text', 'lcov'],
    },
  },
});

Environment Variables

# .env files (loaded in priority order)
.env              # all modes
.env.local        # all modes, gitignored
.env.development  # dev mode only
.env.production   # prod mode only

# Variables must be prefixed VITE_ to be exposed to client
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
SECRET_KEY=server-only  # NOT prefixed, NOT exposed

# Access in code
const apiUrl = import.meta.env.VITE_API_URL;
const isProd = import.meta.env.PROD;    // boolean
const isDev = import.meta.env.DEV;      // boolean
const mode = import.meta.env.MODE;     // 'development' or 'production'

Vitest

// Vitest — Jest-compatible test runner built on Vite
// Same API as Jest: describe, it, test, expect, vi (replaces jest)
import { describe, it, expect, vi } from 'vitest';

describe('utils', () => {
  it('should add numbers', () => {
    expect(add(1, 2)).toBe(3);
  });

  it('should mock module', async () => {
    vi.mock('./api', () => ({
      fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
    }));
    const user = await fetchUser(1);
    expect(user.name).toBe('Alice');
  });
});

// Run
// vitest          — watch mode
// vitest run      — single run (CI)
// vitest --ui     — browser UI
// vitest --coverage

CLI Commands

npm create vite@latest my-app -- --template react-ts

vite                    # start dev server
vite build              # production build
vite preview            # preview production build locally
vite --port 4000        # custom port
vite build --watch      # watch for changes
Vite

Vite Setup & Configuration

Vite Setup & Configuration Vite is a next-generation frontend build tool. It uses native ES modules for instant dev server starts and Rollup for optimized produ

Vite Setup & Configuration

Vite is a next-generation frontend build tool. It uses native ES modules for instant dev server starts and Rollup for optimized production builds.

Scaffolding & Installation

# Create new project
npm create vite@latest my-app -- --template react-ts
npm create vite@latest my-app -- --template vue-ts
npm create vite@latest my-app -- --template svelte-ts

# Install dependencies
cd my-app && npm install

# Dev server
npm run dev

# Production build
npm run build

# Preview production build locally
npm run preview

vite.config.ts — Full Setup

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

export default defineConfig(({ command, mode }) => {
  // Load .env files based on mode
  const env = loadEnv(mode, process.cwd(), '');

  return {
    plugins: [
      react(),                        // React fast refresh (Babel or SWC)
      // react({ jsxRuntime: 'automatic' })
      // @vitejs/plugin-vue            — for Vue 3
      // @vitejs/plugin-svelte         — for Svelte
    ],

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

    server: {
      port: 3000,
      open: true,                     // auto-open browser
      strictPort: true,               // fail if port is taken
      cors: true,
      proxy: {
        '/api': {
          target: 'http://localhost:8000',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
        '/ws': {
          target: 'ws://localhost:8000',
          ws: true,
        },
      },
    },

    build: {
      outDir: 'dist',
      sourcemap: true,
      minify: 'esbuild',              // 'terser' for more aggressive minification
      target: 'esnext',
      chunkSizeWarningLimit: 1000,    // KB
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom'],
            router: ['react-router-dom'],
            ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
          },
        },
      },
    },

    css: {
      modules: {
        localsConvention: 'camelCase',
      },
      preprocessorOptions: {
        scss: {
          additionalData: '@import "@/styles/variables.scss";',
        },
      },
    },

    define: {
      __APP_VERSION__: JSON.stringify(env.npm_package_version),
    },
  };
});

Plugins — React, Vue, PWA

import react from '@vitejs/plugin-react';
import reactSwc from '@vitejs/plugin-react-swc'; // faster — uses SWC instead of Babel
import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa';
import tsconfigPaths from 'vite-tsconfig-paths'; // auto-resolve TS paths

plugins: [
  // React (choose one)
  react(),
  reactSwc(),

  // Vue
  vue(),

  // PWA — generates service worker + manifest
  VitePWA({
    registerType: 'autoUpdate',
    manifest: {
      name: 'My App',
      short_name: 'App',
      theme_color: '#ffffff',
      icons: [
        { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
        { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
      ],
    },
    workbox: {
      runtimeCaching: [
        { urlPattern: /\/api\/.*/, handler: 'NetworkFirst' },
      ],
    },
  }),

  // Auto-resolve paths from tsconfig.json
  tsconfigPaths(),
]

Environment Variables

# .env files — loaded in priority order
.env                  # all modes
.env.local            # all modes, git-ignored
.env.development      # dev mode only (vite dev)
.env.production       # prod mode only (vite build)
.env.staging          # custom mode: vite build --mode staging

# Variables MUST be prefixed VITE_ to be exposed to client code
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
VITE_FEATURE_FLAG_DARK_MODE=true

# NOT prefixed — server-only, never exposed to client
DATABASE_URL=postgresql://...
SECRET_KEY=server-only-value
// Access env vars in client code
const apiUrl = import.meta.env.VITE_API_URL;       // string | undefined
const appTitle = import.meta.env.VITE_APP_TITLE;

// Built-in Vite env vars (always available)
const isProd = import.meta.env.PROD;               // boolean
const isDev = import.meta.env.DEV;                 // boolean
const mode = import.meta.env.MODE;                 // 'development' | 'production' | custom
const baseUrl = import.meta.env.BASE_URL;          // base URL from config

// TypeScript: declare env types in vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_TITLE: string;
  readonly VITE_FEATURE_FLAG_DARK_MODE: string;
}
interface ImportMeta {
  readonly env: ImportMetaEnv;
}
Vite

Vite Features & Optimization

Vite Features & Optimization Vite provides HMR, code splitting, library mode, and multiple build targets. This page covers advanced configuration for production

Vite Features & Optimization

Vite provides HMR, code splitting, library mode, and multiple build targets. This page covers advanced configuration for production-ready applications.

HMR — Hot Module Replacement

// Vite's HMR API — accept updates for a module
if (import.meta.hot) {
  // Accept self — when this module changes, re-execute it
  import.meta.hot.accept((newModule) => {
    if (newModule) {
      newModule.render();
    }
  });

  // Accept updates from a dependency
  import.meta.hot.accept('./dep.ts', (newDep) => {
    console.log('dep updated', newDep);
  });

  // Cleanup side effects before module is replaced
  import.meta.hot.dispose((data) => {
    data.intervalId = setInterval(() => {}, 1000);
  });

  // Decline HMR for this module — full page reload instead
  import.meta.hot.decline();

  // Invalidate — force full reload
  import.meta.hot.invalidate('module state is stale');
}

// HMR data — persist data across updates
import.meta.hot.data.count ??= 0;
import.meta.hot.data.count++;

Code Splitting & Dynamic Imports

// Dynamic import — creates a separate chunk automatically
const { default: Chart } = await import('./Chart');

// React lazy + Suspense
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// Prefetch hint — loads chunk in background
const prefetch = () => import(/* @vite-ignore */ './HeavyPage');
onMouseEnter={() => prefetch()};

Build Optimization & Manual Chunks

// vite.config.ts — advanced build optimization
build: {
  rollupOptions: {
    output: {
      // Manual chunk splitting
      manualChunks(id) {
        if (id.includes('node_modules')) {
          if (id.includes('react')) return 'react-vendor';
          if (id.includes('@radix-ui')) return 'radix-vendor';
          if (id.includes('framer-motion')) return 'animation-vendor';
          return 'vendor';             // everything else into vendor
        }
      },
      // Or use object form for explicit grouping:
      // manualChunks: {
      //   vendor: ['react', 'react-dom', 'react-router-dom'],
      // },
      chunkFileNames: 'assets/[name]-[hash].js',
      entryFileNames: 'assets/[name]-[hash].js',
      assetFileNames: 'assets/[name]-[hash][extname]',
    },
  },
  // Dependency pre-bundling control
  optimizeDeps: {
    include: ['lodash-es', 'date-fns'],    // force pre-bundle
    exclude: ['your-local-lib'],           // skip pre-bundling
  },
}

Library Mode

// Build a reusable library (component library, SDK)
import { resolve } from 'path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';     // generates .d.ts declarations

export default defineConfig({
  plugins: [dts({ include: ['src'] })],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyLib',
      formats: ['es', 'cjs', 'umd'],   // output formats
      fileName: (format) => `my-lib.${format}.js`,
    },
    rollupOptions: {
      // Externalize peer deps — don't bundle React into the lib
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
});

// package.json for the library
// {
//   "main": "./dist/my-lib.cjs.js",
//   "module": "./dist/my-lib.es.js",
//   "types": "./dist/index.d.ts",
//   "exports": {
//     ".": {
//       "import": "./dist/my-lib.es.js",
//       "require": "./dist/my-lib.cjs.js"
//     }
//   }
// }

Vitest — Co-located Test Runner

// Vitest config inside vite.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,                    // no need to import describe/it/expect
    environment: 'jsdom',             // 'node' | 'jsdom' | 'happy-dom'
    setupFiles: ['./src/test/setup.ts'],
    include: ['**/*.{test,spec}.{ts,tsx}'],
    exclude: ['node_modules', 'dist', 'e2e'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov', 'html'],
      thresholds: { lines: 80, branches: 75 },
    },
    // Mock CSS modules and static assets
    css: false,
  },
});

// src/test/setup.ts
import '@testing-library/jest-dom';

// Example test
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from '@/components/Button';

describe('Button', () => {
  it('calls onClick when clicked', async () => {
    const onClick = vi.fn();
    render(<Button onClick={onClick}>Click me</Button>);
    await userEvent.click(screen.getByRole('button'));
    expect(onClick).toHaveBeenCalledOnce();
  });
});

// CLI
// vitest              — watch mode
// vitest run          — single run (CI)
// vitest --ui         — browser UI
// vitest --coverage   — with coverage

Keep your Vite knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever