Getting Started with Vite: Fast JavaScript Build Tool Setup
Overview
Getting started with Vite is straightforward: it is a frontend build tool that ships fast. Its dev server serves files over native ES modules, with no bundling required during development. When you build for production, Vite uses Rollup to bundle your code. The result is a dev experience measured in milliseconds for hot module replacement, and optimized bundles for deployment.
Vite works with plain JavaScript, TypeScript, JSX, CSS preprocessors, and most popular frameworks out of the box. If you are coming from webpack, the mental model shift is significant: Vite does not bundle your code during development, which means your dev server startup and hot updates stay fast regardless of project size.
Project Setup
Scaffolding a new project
npm create vite@latest my-project
cd my-project
npm install
npm run dev
The scaffolded project includes a working dev server, a build script, and placeholder source files. Choose your framework at the prompt and you get preconfigured tooling for that framework. Vite has official templates for vanilla JS, React, Vue, Svelte, and more. If you prefer to set things up from scratch, a manual install takes only one dependency.
Manual installation
When you already have an existing project and want to add Vite incrementally, install the package as a dev dependency:
npm install -D vite
Once installed, you need to register the Vite CLI commands in your project. Add these three scripts to package.json so that npm run dev, npm run build, and npm run preview all work without needing npx:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
npm run dev starts the dev server. npm run build creates the production bundle. npm run preview serves the production build locally so you can verify it before deploying.
The dev server
The dev server starts fast because Vite does not bundle your code. Instead, it serves your source files as native ES modules. When the browser requests a module, Vite transforms and serves it on demand.
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3000,
open: true, // open browser on start
},
});
Run npm run dev and visit http://localhost:3000. The open: true setting opens the browser automatically, which is convenient during local development. The port and other server options can be adjusted per environment using Vite’s environment variable support.
Hot module replacement
HMR is where Vite’s architecture really shines. When you edit a file, Vite only recompiles that specific module and sends the update to the browser. The browser applies the change without a full page reload. For React, Vue, and Svelte, this means your component state is preserved across edits. You can control how HMR behaves through the server configuration:
// vite.config.js
export default defineConfig({
server: {
hmr: {
overlay: true, // show errors in an overlay, not the console
},
},
});
The overlay setting shows runtime errors directly in the browser viewport instead of hiding them in the console. This makes debugging faster since you notice problems immediately rather than discovering them later. Keep it enabled during development and disable it in CI or when you need to test error boundaries.
Proxy configuration
When your frontend and backend run on different ports during development, you hit CORS errors. Instead of configuring CORS headers on every API endpoint, Vite can proxy API requests to your backend server transparently:
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});
Requests to /api/users in the browser get forwarded to http://localhost:8080/api/users. This works for any HTTP method — GET, POST, PUT, DELETE, and WebSocket upgrades. The proxy also handles path rewriting if you need to strip a prefix before forwarding. When your backend runs on the same machine, proxy configuration eliminates the most common source of browser-network friction during development.
Configuration
All configuration lives in vite.config.js (or .ts if using the TypeScript config format). defineConfig() is a helper that gives you TypeScript autocomplete, and the config object covers plugins, path resolution, and build options in one place:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': '/src',
},
},
build: {
outDir: 'dist',
sourcemap: true,
},
});
Build Output
Production bundle
npm run build
Vite uses Rollup under the hood. It produces a highly optimized bundle with:
- Code splitting by route or dynamic import
- Tree shaking of unused code
- CSS extraction and minification
- Asset hashing for cache busting
The output goes to dist/ by default.
Preview the production build
npm run preview
This serves the dist/ folder locally. The preview uses the same production bundle that would be deployed, so no dev server quirks slip through.
Working with TypeScript
Vite handles TypeScript without a separate compiler step. It uses esbuild to transpile TypeScript to JavaScript, which is significantly faster than tsc. Install TypeScript as a dev dependency and add the type-check step to your workflow:
npm install -D typescript
npx tsc --noEmit
Type checking still requires running tsc separately in a CI step. Vite does not perform type checking during the build; it only strips types. Run tsc --noEmit in your CI pipeline to catch type errors without slowing down your dev server or production build.
CSS and Preprocessors
Import CSS directly in your JavaScript files. Vite resolves the import, processes the stylesheet, and injects it into the document at runtime during development:
import './styles/main.css';
For component-scoped styles, Vite supports CSS modules out of the box. Any file ending in .module.css is treated as a CSS module, and Vite returns an object mapping class names to unique identifiers:
import styles from './button.module.css';
If you need the raw CSS string instead of injected styles, append ?inline to the import path. This gives you the stylesheet content as a string, which is useful when you need to pass styles to a shadow DOM or a third-party library that expects a CSS string:
import styles from './button.css?inline';
For Sass or Less, install the preprocessor and Vite compiles the files automatically. No extra plugin or loader configuration is needed:
npm install -D sass
Then write Sass as usual. Vite compiles it automatically during both development and production builds. The same pattern works for Less and Stylus — install the corresponding package and Vite handles the rest without touching your config file.
Environment Variables
Vite exposes environment variables through import.meta.env. Define variables in .env files at the project root, using the VITE_ prefix:
// .env
VITE_API_URL=https://api.example.com
VITE_VERSION=1.0.0
In your application code, access these values through import.meta.env:
// In your code
console.log(import.meta.env.VITE_API_URL); // https://api.example.com
console.log(import.meta.env.VITE_VERSION); // 1.0.0
Variables must be prefixed with VITE_ to be exposed to client-side code. Variables without this prefix are ignored, which prevents accidentally leaking server secrets to the browser.
For different environments, Vite loads environment-specific files automatically based on the mode. The .env.local file is git-ignored by convention and holds secrets or machine-specific overrides:
.env # default
.env.development # development (npm run dev)
.env.production # production (npm run build)
.env.local # local overrides, git-ignored
Plugins
Vite’s plugin API is compatible with Rollup’s, which means many Rollup plugins work in Vite without modification. The official framework plugins add framework-specific features like Fast Refresh for React and HMR for Vue single-file components.
Official React plugin
Install the plugin and add it to your Vite config:
npm install -D @vitejs/plugin-react
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});
This plugin enables Fast Refresh for React components, so edits update without losing state. It also handles JSX transforms automatically — no Babel configuration needed.
Official Vue plugin
npm install -D @vitejs/plugin-vue
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
});
The Vue plugin provides SFC compilation, template compilation, and HMR for Vue components. If you use <script setup>, the plugin handles the compile-time transform that powers that syntax.
Other common plugins
The Vite ecosystem includes plugins for legacy browser support, progressive web apps, and additional framework integrations:
npm install -D @vitejs/plugin-legacy # ES5 fallback for old browsers
npm install -D vite-plugin-pwa # PWA support
npm install -D vite-plugin-solid # Solid.js support
Code splitting and lazy loading
Vite handles code splitting automatically when you use dynamic import(). Instead of bundling everything into a single file, Vite splits the dynamically imported module into a separate chunk that loads on demand:
// Instead of:
import { heavyFunction } from './heavy.js';
// Use:
const heavy = await import('./heavy.js');
heavy.heavyFunction();
The module is loaded only when heavy.heavyFunction() is actually called. Vite splits this into a separate chunk automatically.
For React, this pairs well with React.lazy():
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard.jsx'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
);
}
Common Gotchas
Dev server vs production bundle differences. Vite does not bundle in dev, which means some patterns behave differently. If a library works in dev but breaks in production, it may be relying on bundler-specific behavior. Use vite-plugin-commonjs or report the issue to the library maintainer.
Environment variables are baked in at build time. Changing .env values after vite build has no effect; the values are embedded in the bundle. For runtime config, embed values in the HTML file or load them from a separate endpoint.
The ?url suffix for raw assets. To import an asset as a URL string rather than having Vite process it as a module:
import myFile from './file.txt?url';
// myFile is now a string like /assets/file.abc123.txt
Without ?url, Vite processes the file as a module. This suffix is commonly used for images, fonts, and other static assets that need a URL at runtime rather than being inlined into the JavaScript bundle.
Monorepo setups need workspace awareness. If your project spans multiple packages, you may need to configure optimizeDeps.include to pre-bundle dependencies that are imported across packages:
export default defineConfig({
optimizeDeps: {
include: ['some-shared-lib'],
},
});
Keep the dev loop focused
Vite works best when the project treats the dev server as the fast feedback loop and the production build as the final packaging step. That separation keeps local work smooth and makes it easier to notice when a change only works in one mode. If something behaves differently after vite build, check whether the code depends on a dev-only assumption. A clean split between local serving and final output makes bugs easier to track down.
Treat config as project code
vite.config.js should evolve with the app, not sit there as a pile of forgotten defaults. Alias settings, plugins, and proxy rules all shape how the team works every day. When a setting exists, it should solve a real problem and be readable enough that the next person can explain it back. That habit keeps the toolchain understandable when the project grows or when multiple developers need to share the same setup.
Make environment choices explicit
Vite projects tend to grow faster when the environment split is visible. Development-only settings, production-only optimizations, and shared defaults should all be easy to spot. That makes it harder for a small local tweak to accidentally leak into the shipped bundle. It also helps new contributors understand where to look when something behaves differently between vite dev and vite build, which is a common source of confusion.
Keep the build contract simple
When the build contract is clear, the team can think about features instead of toolchain trivia. A short list of aliases, a predictable set of plugins, and a known asset pipeline are usually enough for most applications. If the config starts to mirror the shape of the entire app, that is a sign to step back and remove a layer. Simplicity here pays off every time the project gets a new dependency or a new deploy path.
See Also
- /guides/javascript-modules-esm/ — ESM syntax that Vite uses natively in the dev server
- /guides/javascript-node-best-practices/ — Node.js tooling conventions that pair well with Vite
- /tutorials/testing-javascript/testing-with-vitest/ — test your Vite project with the Vite-native test runner