Tooling

Vite for Development: Fast Builds and HMR

A practical guide to Vite covering fast development server, HMR, esbuild pre-bundling, production builds with Rollup, and migration from webpack.

Vite for Development: Fast Builds and HMR

Overview

Vite is a build tool that fundamentally rethinks how a development server should work. Instead of bundling your entire application before serving it -- like webpack does -- Vite serves source files over native ES modules and only transforms code on demand. The result is a dev server that starts in milliseconds regardless of application size, and hot module replacement that stays fast as your codebase grows to thousands of modules. For production builds, Vite uses Rollup under the hood, producing optimized bundles with tree-shaking, code splitting, and asset hashing.

I switched a 1,200-module React application from webpack to Vite and the dev server cold start went from 28 seconds to 400 milliseconds. HMR updates that took 2-3 seconds under webpack became effectively instant. If you are still using webpack for new projects in 2026, you are paying a tax on every single file save.

Prerequisites

  • Node.js 18 or later (Node 20+ recommended)
  • npm, yarn, or pnpm
  • Basic understanding of ES modules and bundling concepts
  • An existing JavaScript project or willingness to scaffold a new one

Why Vite Is Fast

The speed advantage comes from two architectural decisions that webpack fundamentally cannot replicate without rewriting its core.

Native ESM Dev Server

Traditional bundlers like webpack crawl your entire dependency graph, transform every module, and stitch them into a bundle before the browser sees anything. Vite skips this entirely during development. It serves your source files as native ES modules over HTTP. When the browser encounters an import statement, it sends a request to the Vite dev server, which transforms that single file on the fly using esbuild (for TypeScript, JSX) and returns it. The browser's own module loader handles the dependency resolution.

This means startup time is constant -- it does not scale with application size. Whether you have 50 modules or 5,000, the server starts in the same time because it is not processing anything upfront.

esbuild Pre-Bundling

There is one exception to the "no bundling in dev" rule: dependencies in node_modules. Most npm packages are published as CommonJS, and even ESM packages can have hundreds of internal modules (lodash-es has over 600 individual files). Loading 600 separate HTTP requests for a single import would be slow. Vite pre-bundles dependencies using esbuild, which is 10-100x faster than JavaScript-based bundlers because it is written in Go.

The first time you run vite, it scans your source code for bare module imports, pre-bundles them with esbuild, and caches the result in node_modules/.vite. Subsequent starts skip this step entirely. You can force a re-bundle with --force:

npx vite --force

The pre-bundle also converts CommonJS modules to ESM so the browser can load them natively.

Project Setup

Scaffolding a New Project

npm create vite@latest my-app -- --template vanilla
cd my-app
npm install
npm run dev

Available templates include vanilla, vanilla-ts, react, react-ts, vue, vue-ts, svelte, svelte-ts, preact, preact-ts, lit, and lit-ts.

Adding Vite to an Existing Project

npm install --save-dev vite

Create a minimal index.html in your project root:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>My App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

The index.html is the entry point. Vite uses it to discover your module graph. This is different from webpack where you configure entry points in a config file.

vite.config.js

The configuration file uses ES module syntax even if the rest of your project uses CommonJS. Vite handles this automatically.

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  root: './',
  base: '/',
  publicDir: 'public',
  build: {
    outDir: 'dist',
    sourcemap: true,
    minify: 'esbuild',
    target: 'es2020',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash-es', 'date-fns']
        }
      }
    }
  },
  server: {
    port: 3000,
    strictPort: true,
    open: true,
    cors: true
  },
  preview: {
    port: 4173
  }
});

The defineConfig helper provides TypeScript intellisense even in plain JavaScript files. Every option has sensible defaults -- most projects need fewer than 10 lines of configuration.

Dev Server Configuration

Proxying API Requests

When your frontend runs on localhost:3000 and your Express.js backend runs on localhost:8080, you need a proxy to avoid CORS issues during development. Vite has this built in:

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        secure: false
      },
      '/auth': {
        target: 'http://localhost:8080',
        changeOrigin: true
      },
      '/socket.io': {
        target: 'ws://localhost:8080',
        ws: true
      }
    }
  }
});

Any request from the browser to /api/users hits the Vite dev server, which forwards it to http://localhost:8080/api/users. The changeOrigin flag rewrites the Host header, which matters when your backend checks the origin. The ws: true option enables WebSocket proxying.

You can also use a function for advanced matching:

server: {
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      rewrite: function(path) {
        return path.replace(/^\/api/, '/v2/api');
      }
    }
  }
}

HTTPS in Development

server: {
  https: {
    key: './certs/localhost-key.pem',
    cert: './certs/localhost.pem'
  }
}

Use mkcert to generate locally-trusted certificates:

brew install mkcert   # macOS
mkcert -install
mkcert localhost 127.0.0.1

HMR: How It Works

Hot Module Replacement in Vite is nearly instant because of how little work happens on each update. When you save a file:

  1. The file system watcher detects the change (Vite uses chokidar).
  2. Vite determines which modules are affected by walking the import graph backward from the changed file.
  3. Only the affected modules are invalidated and re-fetched by the browser via WebSocket notification.
  4. The browser re-executes only the invalidated modules.

With webpack, HMR requires re-bundling a chunk that contains the changed module plus all its dependencies. With Vite, the browser fetches a single transformed module.

HMR API

Vite exposes an HMR API through import.meta.hot that you can use in your modules to control how updates are applied:

// src/counter.js
var count = 0;

function setupCounter(element) {
  element.textContent = count;
  element.addEventListener('click', function() {
    count++;
    element.textContent = count;
  });
}

export { setupCounter };

if (import.meta.hot) {
  import.meta.hot.accept(function(newModule) {
    // Handle the updated module
    console.log('Counter module updated');
  });

  // Preserve state across HMR updates
  import.meta.hot.data.count = count;

  import.meta.hot.dispose(function(data) {
    // Cleanup before the old module is replaced
    data.count = count;
  });
}

Framework plugins (React, Vue, Svelte) handle HMR automatically. You only need the HMR API when writing framework-agnostic modules or custom integrations.

Environment Variables

Vite uses .env files with a specific convention. Only variables prefixed with VITE_ are exposed to client code.

# .env
VITE_API_URL=http://localhost:8080
VITE_APP_TITLE=My Application
DB_PASSWORD=secret123

Access them in client code:

// src/api.js
var apiUrl = import.meta.env.VITE_API_URL;
var appTitle = import.meta.env.VITE_APP_TITLE;
var mode = import.meta.env.MODE;        // 'development' or 'production'
var isDev = import.meta.env.DEV;         // true in dev
var isProd = import.meta.env.PROD;       // true in prod
var baseUrl = import.meta.env.BASE_URL;  // the base option from config

// DB_PASSWORD is NOT available -- only VITE_ prefixed vars are exposed

Vite supports multiple .env files loaded in priority order:

.env                # always loaded
.env.local          # always loaded, gitignored
.env.development    # loaded in dev mode
.env.production     # loaded in production build
.env.staging        # loaded with --mode staging

On the server side (Express.js), you continue using process.env and packages like dotenv:

// server.js (Express backend -- CommonJS)
var dotenv = require("dotenv");
dotenv.config();

var express = require("express");
var app = express();
var port = process.env.PORT || 8080;
var dbPassword = process.env.DB_PASSWORD;

app.get("/api/config", function(req, res) {
  res.json({ apiVersion: "2.0" });
});

app.listen(port, function() {
  console.log("Server running on port " + port);
});

CSS Handling

PostCSS

If a postcss.config.js exists in your project root, Vite applies PostCSS transforms automatically. No plugin or extra configuration needed.

// postcss.config.js
module.exports = {
  plugins: {
    autoprefixer: {},
    'postcss-nesting': {}
  }
};

CSS Modules

Any CSS file ending in .module.css is treated as a CSS module:

/* src/components/header.module.css */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}

.title {
  font-size: 2rem;
  font-weight: 700;
  color: #1a1a1a;
}

.navLink {
  color: #4a90d9;
  text-decoration: none;
  transition: color 0.2s;
}

.navLink:hover {
  color: #2c5f8a;
}
// src/components/header.js
import styles from './header.module.css';

function createHeader(title) {
  var header = document.createElement('header');
  header.className = styles.container;

  var h1 = document.createElement('h1');
  h1.className = styles.title;
  h1.textContent = title;
  header.appendChild(h1);

  return header;
}

export { createHeader };

The class names are scoped automatically. styles.container resolves to something like _container_1a2b3_1, preventing CSS collisions across components.

Preprocessors

Install the preprocessor and Vite detects it automatically:

npm install --save-dev sass          # for .scss/.sass
npm install --save-dev less          # for .less
npm install --save-dev stylus        # for .styl

No loader configuration needed. Just import:

import './styles/main.scss';

Static Asset Handling

Imports as URLs

Importing an asset returns its resolved public URL:

import logoUrl from './assets/logo.png';

var img = document.createElement('img');
img.src = logoUrl;  // /assets/logo-a1b2c3d4.png (hashed in production)

The Public Directory

Files in the public/ directory are served at the root path and copied as-is during build. Use this for files that must retain their exact filename (like favicon.ico, robots.txt, or files referenced by third-party libraries):

public/
  favicon.ico
  robots.txt
  og-image.png

Reference them with absolute paths: /favicon.ico, /robots.txt. Do not import them in JavaScript -- they bypass the asset pipeline.

Glob Imports

Load multiple files at once with import.meta.glob:

// Load all markdown files in the posts directory
var modules = import.meta.glob('./posts/*.md');

// modules is an object like:
// {
//   './posts/intro.md': () => import('./posts/intro.md'),
//   './posts/setup.md': () => import('./posts/setup.md')
// }

// Each value is a lazy import function. Load them on demand:
for (var path in modules) {
  modules[path]().then(function(mod) {
    console.log(path, mod);
  });
}

// Or load eagerly:
var eagerModules = import.meta.glob('./posts/*.md', { eager: true });
// eagerModules['./posts/intro.md'] is already resolved

Glob imports are resolved at build time. Vite generates the dynamic import statements, so there is no runtime directory scanning.

Building for Production

Run the production build:

npx vite build

Under the hood, Vite uses Rollup for production builds. Rollup produces smaller bundles than webpack thanks to better tree-shaking and scope hoisting. The output lands in dist/ by default.

Build Output

dist/
  index.html
  assets/
    index-a1b2c3d4.js      # your application code
    vendor-e5f6g7h8.js      # third-party dependencies
    index-i9j0k1l2.css      # extracted CSS
    logo-m3n4o5p6.png        # hashed static assets

All filenames include content hashes for long-term caching. When a file changes, its hash changes and browsers fetch the new version.

Chunk Splitting

Vite automatically code-splits on dynamic import() boundaries. For finer control, use manualChunks:

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: function(id) {
          if (id.includes('node_modules')) {
            // Split each large dependency into its own chunk
            if (id.includes('chart.js')) {
              return 'charts';
            }
            if (id.includes('monaco-editor')) {
              return 'editor';
            }
            // Group remaining vendor code
            return 'vendor';
          }
        }
      }
    }
  }
});

This gives you control over what ships together. Large dependencies like chart libraries or code editors belong in their own chunks so they can be cached independently.

Build Targets

By default, Vite targets browsers that support native ESM, native ESM dynamic import, and import.meta. For older browser support:

build: {
  target: 'es2015',    // or specific browsers
  cssTarget: 'chrome80'
}

For legacy browser support, use the official @vitejs/plugin-legacy plugin, which generates a separate legacy bundle with polyfills.

Library Mode

Vite can also build libraries for npm publishing:

// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.js'),
      name: 'MyLib',
      fileName: function(format) {
        return 'my-lib.' + format + '.js';
      }
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM'
        }
      }
    }
  }
});

This produces both ES module and UMD builds. External dependencies are excluded from the bundle.

Multi-Page Applications

Vite supports multi-page apps by specifying multiple HTML entry points:

// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        dashboard: resolve(__dirname, 'dashboard/index.html'),
        admin: resolve(__dirname, 'admin/index.html')
      }
    }
  }
});

Each HTML file is an independent entry point with its own module graph. Shared dependencies are automatically extracted into common chunks.

Plugins

Vite plugins extend both the dev server and the build. The plugin API is a superset of Rollup's plugin API, so most Rollup plugins work out of the box.

Official Plugins

npm install --save-dev @vitejs/plugin-react       # React Fast Refresh
npm install --save-dev @vitejs/plugin-vue          # Vue 3 SFC support
npm install --save-dev @vitejs/plugin-legacy       # Legacy browser support
// vite.config.js with React
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()]
});

Useful Community Plugins

// vite.config.js
import { defineConfig } from 'vite';
import { compression } from 'vite-plugin-compression2';
import { visualizer } from 'rollup-plugin-visualizer';
import svgr from 'vite-plugin-svgr';

export default defineConfig({
  plugins: [
    compression({ algorithm: 'gzip' }),
    compression({ algorithm: 'brotliCompress' }),
    svgr(),
    visualizer({
      filename: 'stats.html',
      open: true,
      gzipSize: true
    })
  ]
});
  • vite-plugin-compression2 -- generates gzip and brotli compressed assets at build time
  • rollup-plugin-visualizer -- creates a treemap visualization of your bundle (invaluable for finding size issues)
  • vite-plugin-svgr -- import SVGs as components
  • vite-plugin-pwa -- service worker and PWA manifest generation

Complete Working Example: Vanilla JS + Express.js

Here is a full project setup with a Vite frontend, Express.js backend, API proxy, CSS modules, environment variables, and production build configuration.

Project Structure

my-app/
  package.json
  vite.config.js
  .env
  .env.production
  public/
    favicon.ico
  src/
    main.js
    api.js
    components/
      header.module.css
      header.js
    styles/
      global.css
  server/
    index.js

package.json

{
  "name": "my-vite-app",
  "version": "1.0.0",
  "scripts": {
    "dev": "concurrently \"vite\" \"node server/index.js\"",
    "build": "vite build",
    "preview": "vite preview",
    "start": "node server/index.js"
  },
  "devDependencies": {
    "concurrently": "^8.2.0",
    "vite": "^6.0.0"
  },
  "dependencies": {
    "express": "^4.18.0",
    "dotenv": "^16.0.0"
  }
}

vite.config.js

import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  root: './',
  publicDir: 'public',
  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: function(id) {
          if (id.includes('node_modules')) {
            return 'vendor';
          }
        }
      }
    }
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        secure: false
      }
    }
  },
  css: {
    modules: {
      localsConvention: 'camelCase'
    }
  }
});

.env

VITE_API_URL=http://localhost:8080
VITE_APP_TITLE=My Vite App
DB_PASSWORD=dev_secret_123

.env.production

VITE_API_URL=https://api.myapp.com
VITE_APP_TITLE=My App

server/index.js

var dotenv = require("dotenv");
dotenv.config();

var express = require("express");
var path = require("path");
var app = express();
var port = process.env.PORT || 8080;

app.use(express.json());

// Serve static files from Vite build in production
if (process.env.NODE_ENV === "production") {
  app.use(express.static(path.join(__dirname, "..", "dist")));
}

// API routes
app.get("/api/status", function(req, res) {
  res.json({
    status: "ok",
    timestamp: new Date().toISOString(),
    environment: process.env.NODE_ENV || "development"
  });
});

app.get("/api/items", function(req, res) {
  var items = [
    { id: 1, name: "Build Tool", category: "tooling" },
    { id: 2, name: "Dev Server", category: "development" },
    { id: 3, name: "Bundler", category: "production" }
  ];
  res.json({ items: items });
});

// Catch-all for SPA routing in production
if (process.env.NODE_ENV === "production") {
  app.get("*", function(req, res) {
    res.sendFile(path.join(__dirname, "..", "dist", "index.html"));
  });
}

app.listen(port, function() {
  console.log("API server running on http://localhost:" + port);
});

src/main.js

import { createHeader } from './components/header.js';
import { fetchItems } from './api.js';
import './styles/global.css';

var appTitle = import.meta.env.VITE_APP_TITLE || 'App';
var app = document.getElementById('app');

// Render header
var header = createHeader(appTitle);
app.appendChild(header);

// Render items from API
var itemList = document.createElement('ul');
itemList.id = 'item-list';
app.appendChild(itemList);

fetchItems().then(function(items) {
  items.forEach(function(item) {
    var li = document.createElement('li');
    li.textContent = item.name + ' (' + item.category + ')';
    itemList.appendChild(li);
  });
}).catch(function(err) {
  console.error('Failed to fetch items:', err);
  itemList.textContent = 'Failed to load items.';
});

src/api.js

var baseUrl = import.meta.env.VITE_API_URL || '';

function fetchItems() {
  return fetch(baseUrl + '/api/items')
    .then(function(response) {
      if (!response.ok) {
        throw new Error('HTTP ' + response.status);
      }
      return response.json();
    })
    .then(function(data) {
      return data.items;
    });
}

function fetchStatus() {
  return fetch(baseUrl + '/api/status')
    .then(function(response) {
      return response.json();
    });
}

export { fetchItems, fetchStatus };

src/components/header.js

import styles from './header.module.css';

function createHeader(title) {
  var header = document.createElement('header');
  header.className = styles.container;

  var h1 = document.createElement('h1');
  h1.className = styles.title;
  h1.textContent = title;

  var nav = document.createElement('nav');
  var links = ['Home', 'About', 'Contact'];
  links.forEach(function(text) {
    var a = document.createElement('a');
    a.className = styles.navLink;
    a.href = '#' + text.toLowerCase();
    a.textContent = text;
    nav.appendChild(a);
  });

  header.appendChild(h1);
  header.appendChild(nav);
  return header;
}

export { createHeader };

Run npm run dev and both the Vite dev server (port 3000) and Express API (port 8080) start. The proxy forwards /api/* requests from Vite to Express. For production, run npm run build then NODE_ENV=production npm start to serve the static build from Express.

Migrating from Webpack to Vite

I have migrated five webpack projects to Vite. Here is the process that works.

Step 1: Install Vite, Remove Webpack

npm install --save-dev vite
npm uninstall webpack webpack-cli webpack-dev-server \
  html-webpack-plugin css-loader style-loader \
  babel-loader file-loader url-loader mini-css-extract-plugin

Step 2: Move index.html to Root

Webpack uses HtmlWebpackPlugin to generate HTML. Vite uses a real index.html file in the project root as the entry point. Move your HTML template to the root and add a module script tag pointing at your entry file.

Step 3: Replace webpack.config.js with vite.config.js

Common webpack-to-Vite mappings:

Webpack Vite
entry <script> tag in index.html
output.path build.outDir
output.publicPath base
devServer.proxy server.proxy
resolve.alias resolve.alias
module.rules (CSS) Automatic, no config needed
module.rules (images) Automatic, no config needed
DefinePlugin .env files + define option
HtmlWebpackPlugin Built-in (index.html is the entry)

Step 4: Update Imports

Replace webpack-specific imports:

// Webpack style (remove these)
require('./styles.css');
var logo = require('./logo.png');

// Vite style
import './styles.css';
import logo from './logo.png';

Step 5: Replace process.env with import.meta.env

// Before (webpack)
var apiUrl = process.env.REACT_APP_API_URL;

// After (Vite)
var apiUrl = import.meta.env.VITE_API_URL;

Rename your environment variables from REACT_APP_* to VITE_*.

Vite vs Webpack Comparison

Aspect Vite Webpack
Dev server startup ~300ms (constant) 10-60s (scales with app size)
HMR speed <50ms 200ms-3s
Config complexity Minimal Extensive
Production bundler Rollup webpack
CSS handling Built-in Requires loaders
TypeScript Built-in (esbuild) Requires ts-loader or babel
Code splitting Automatic on import() Automatic on import()
Tree shaking Excellent (Rollup) Good
Plugin ecosystem Growing rapidly Massive, mature
Legacy browser support Via plugin Built-in
SSR support Experimental but usable Via plugins

Webpack still wins on plugin ecosystem breadth and edge-case handling. If you have a complex webpack config with custom loaders doing non-standard transforms, the migration may require writing custom Vite plugins. For 90% of projects, though, Vite is the better choice today.

Common Issues and Troubleshooting

1. "Failed to resolve import" for CommonJS Packages

[vite] Internal server error: Failed to resolve import "some-cjs-package"

Some CommonJS packages do not work with Vite's ESM dev server out of the box. Add them to the optimizeDeps.include list to force pre-bundling:

// vite.config.js
optimizeDeps: {
  include: ['some-cjs-package', 'another-legacy-package']
}

2. "process is not defined" in Browser Code

Uncaught ReferenceError: process is not defined

Libraries that reference process.env.NODE_ENV will break because Vite does not inject a process global. Fix it with the define option:

// vite.config.js
define: {
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
}

Better yet, update the library or switch to one that uses import.meta.env.

3. CORS Errors During Development

Access to fetch at 'http://localhost:8080/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy

You are calling the backend directly instead of going through the proxy. Make sure your API calls use relative paths (/api/data) not absolute URLs (http://localhost:8080/api/data). The proxy only intercepts requests to the Vite dev server.

4. CSS Modules Not Working

import styles from './component.css';
// styles is undefined or a plain string

The file must end in .module.css, not just .css. Rename it to component.module.css. Regular .css files are treated as global stylesheets.

5. Pre-Bundled Dependency Stale After Update

[vite] error while updating dependencies:
  Could not resolve "package-x" - The package may have been removed

Clear the dependency cache and restart:

rm -rf node_modules/.vite
npx vite --force

This forces esbuild to re-scan and re-bundle all dependencies.

6. Build Produces Empty Chunk Warnings

(!) Some chunks are larger than 500 kB after minification.

This is a warning, not an error. Fix it with manual chunks to break up large vendor bundles:

build: {
  chunkSizeWarningLimit: 600,
  rollupOptions: {
    output: {
      manualChunks: function(id) {
        if (id.includes('node_modules/chart.js')) return 'charts';
        if (id.includes('node_modules')) return 'vendor';
      }
    }
  }
}

Best Practices

  1. Always use relative paths for API calls in development. Let the proxy handle routing to your backend. Hardcoding http://localhost:8080 in fetch calls bypasses the proxy and causes CORS errors.

  2. Commit your .env.example but gitignore .env and .env.local. Include all required variable names with placeholder values so new developers know what to configure.

  3. Use manualChunks for large dependencies. Libraries like Monaco Editor, Chart.js, or PDF.js should be in their own chunks so they cache independently and do not block initial page load.

  4. Run npx vite build --mode staging for staging environments. Create a .env.staging file with staging-specific variables. The --mode flag controls which .env file is loaded.

  5. Install rollup-plugin-visualizer and review your bundle on every major dependency addition. It generates a treemap that makes bloated dependencies immediately obvious. I have caught multiple 500KB+ transitive dependencies this way before they shipped.

  6. Use build.sourcemap: true in staging, 'hidden' in production. Hidden source maps are generated but not referenced in the output, so they can be uploaded to error tracking services like Sentry without exposing source code to end users.

  7. Keep vite.config.js minimal. If your config is over 50 lines, you are probably configuring things that Vite handles automatically. Resist the webpack instinct to configure everything explicitly.

  8. Use import.meta.glob instead of custom webpack contexts. It is type-safe, tree-shakeable, and produces cleaner code than require.context().

References

Powered by Contentful