
What Vite actually does in a Vue project, from dev server to production build, and why it replaced Vue CLI.
Reza Baar
May 13, 2026
When you scaffold a Vue project today with pm create vue@latest, you get Vite as the build tool. But what does Vite actually do? And why did the Vue ecosystem move away from Vue CLI and webpack? In this post, we'll look at each piece of what Vite handles in a Vue project and why it matters.
First, an important distinction. Vite is a general-purpose build tool. It works with React, Svelte, Solid, and plain JavaScript too. What makes it work with Vue is a plugin: @vitejs/plugin-vue.
When you create a Vue project, the generated vite.config.ts looks like this:
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
});
Vite itself provides the dev server, the build pipeline, and the module system. The Vue plugin teaches Vite how to understand .vue files. Without the plugin, Vite has no idea what a Single File Component is.
When you run npm run dev, Vite starts a dev server. Here's what it does differently from the webpack-based Vue CLI.
Vue CLI with webpack would bundle your entire app before serving it. For large projects, this meant waiting 30 seconds or more for the dev server to start.
Vite doesn't bundle in development. Instead, it serves your source files directly as native ES modules. When the browser requests a file, Vite transforms it on demand and sends it back.
Browser requests /src/App.vue
→ Vite intercepts the request
→ @vitejs/plugin-vue compiles the SFC
→ Vite sends back JavaScript the browser can execute
The startup is nearly instant because Vite only processes files as they're requested, not the entire dependency graph upfront.
There's one exception to the "no bundling" rule. Your node_modules dependencies (Vue itself, any UI library, etc.) are pre-bundled using esbuild on first startup. This happens because:
lodash-es has hundreds of internal modules that would mean hundreds of HTTP requestsVite bundles these into single ES modules and caches them in node_modules/.vite. This happens once and is cached until your dependencies change.
# You'll see this on first startup
Pre-bundling dependencies:
vue
vue-router
pinia
The @vitejs/plugin-vue plugin is responsible for turning .vue files into JavaScript. A Single File Component like this:
<!-- src/components/Counter.vue -->
<script setup lang="ts">
const count = ref(0);
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
<style scoped>
button {
font-size: 1.2rem;
}
</style>
Gets compiled into roughly three parts:
<script> block becomes a JavaScript module<template> block is compiled into a render function by @vue/compiler-sfc<style scoped> block is extracted and injected as CSS with scoped selectorsThe compiled output looks something like:
// Simplified compiled output
import { ref, openBlock, createElementBlock, toDisplayString } from 'vue';
const _sfc_main = {
setup() {
const count = ref(0);
return { count };
},
};
function _sfc_render(_ctx) {
return (
openBlock(),
createElementBlock(
'button',
{ onClick: () => _ctx.count++ },
'Count: ' + toDisplayString(_ctx.count)
)
);
}
// Style is injected separately with scoped data attributes
You never see this output directly. Vite handles it transparently.
This is the feature you interact with most. When you edit a .vue file and save, the change appears in the browser almost instantly without a full page reload.
Vite's HMR works at the module level. When you change Counter.vue, Vite only recompiles that one file and sends the update over a WebSocket connection to the browser. The Vue plugin hooks into Vue's component system to hot-swap the component while preserving its state.
You edit Counter.vue
→ Vite detects the file change
→ @vitejs/plugin-vue recompiles just that SFC
→ Vite sends a HMR update via WebSocket
→ The browser replaces the component
→ Component state (like count) is preserved
With webpack, HMR updates slowed down as projects grew because the bundler had to re-analyze the dependency graph. Vite's approach stays fast regardless of project size because it only processes the changed file.
When you run npm run build, Vite switches from its dev-mode behavior to a production build powered by Rollup (for now - March 2026).
// vite.config.ts
export default defineConfig({
plugins: [vue()],
build: {
target: 'es2015',
outDir: 'dist',
},
});
The build pipeline:
import() boundariesThe output lands in dist/:
dist/
index.html
assets/
index-abc123.js
index-def456.css
vendor-ghi789.js
Vite and Rollup handle code splitting automatically when you use dynamic imports. With Vue Router, lazy-loaded routes create separate chunks:
// src/router/index.ts
const routes = [
{
path: '/',
component: () => import('../pages/Home.vue'),
},
{
path: '/about',
component: () => import('../pages/About.vue'),
},
{
path: '/dashboard',
component: () => import('../pages/Dashboard.vue'),
},
];
Each import() becomes a separate chunk. The browser only downloads the code for the route the user visits. Vite handles the chunk naming and loading automatically.
Vite supports TypeScript out of the box. It uses esbuild for TypeScript transpilation, which is extremely fast. But there's an important detail: Vite strips types without type-checking.
// This compiles fine in Vite even if the type is wrong
const count: string = 42;
Vite prioritizes speed over correctness during development. Type checking is left to your editor (VS Code with Volar or now knows is “Vue - Official”) and a separate vue-tsc step in CI:
// package.json
{
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"type-check": "vue-tsc --noEmit"
}
}
The build script runs type checking first with vue-tsc, then builds with Vite. This keeps the dev experience fast while catching type errors before production.
Vite handles imports of static assets like images, fonts, and SVGs:
<script setup lang="ts">
import logoUrl from '../assets/logo.svg';
</script>
<template>
<img :src="logoUrl" alt="Logo" />
</template>
During dev, logoUrl resolves to the file path. During build, the file is copied to dist/assets/ with a content hash, and the import resolves to the hashed filename.
For assets in the public/ directory, they're served as-is without processing:
<template>
<!-- Served directly from /public/favicon.ico -->
<link rel="icon" href="/favicon.ico" />
</template>
Vite exposes environment variables through import.meta.env:
# .env
VITE_APP_TITLE=My Vue App
VITE_API_URL=https://api.example.com
console.log(import.meta.env.VITE_APP_TITLE); // "My Vue App"
console.log(import.meta.env.MODE); // "development" or "production"
console.log(import.meta.env.DEV); // true in dev
console.log(import.meta.env.PROD); // true in production build
Only variables prefixed with VITE_ are exposed to client code. This prevents accidentally leaking server secrets into the browser bundle.
Vite's plugin API is compatible with Rollup's, which means the Rollup plugin ecosystem works with Vite too. Common Vue-related plugins:
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import AutoImport from 'unplugin-auto-import/vite';
export default defineConfig({
plugins: [
vue(),
Components({
// Auto-import components from ./src/components/
}),
AutoImport({
imports: ['vue', 'vue-router'],
// Auto-import Vue and Vue Router APIs
}),
],
});
These plugins replicate some of what Nuxt gives you for free (auto-imports, component resolution) in a plain Vue project.
Vue CLI was built on webpack. It worked well for years, but had fundamental speed limitations:
Vite solved these by using native ES modules in dev and esbuild for transforms. The Vue team officially deprecated Vue CLI in favor of create-vue (which scaffolds Vite projects) and Vite is the recommended build tool in the Vue documentation.
@vitejs/plugin-vue is what makes it understand .vue files@vitejs/plugin-vue compiles SFCs into render functions, JavaScript modules, and scoped CSSvue-tsc for thatVite handles the entire dev-to-production pipeline for Vue projects: serving files in development, compiling SFCs, hot-reloading changes, and building optimized bundles. Understanding what it does at each step helps you debug build issues and configure your project with intention rather than copying config snippets.
I hope this post has been helpful. Please let me know if you have any questions or comments. Happy coding!
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

The Role Vite Plays in Vue
What Vite actually does in a Vue project, from dev server to production build, and why it replaced Vue CLI.
Reza Baar
May 13, 2026

JavaScript finally gets dates right
JavaScript's Date object is 30 years old, copied from Java, and never really fixed. Temporal is the native API that finally gets dates right: immutable, timezone-aware, and no more dividing by 86400000.
Martin Ferret
May 12, 2026

Security in React Applications
Learn how to secure React apps: prevent XSS with DOMPurify, store tokens safely in HttpOnly cookies, validate server inputs with Zod, and configure Content Security Policy.
Aurora Scharff
May 7, 2026
We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.
Let us help you get the resources you need.
