Why Vite Beats Webpack for Vue 3: Fast Startup, HMR, and On‑Demand Compilation
This article explains how Vite, built for Vue 3, solves Webpack's slow cold starts, sluggish hot updates, and heavy bundling by leveraging native ES Modules, a lightweight Koa server, and on‑demand compilation, while also comparing Vite with Snowpack.
Liang Xiaoying, a piggy girl who loves swimming & reading.
Are you tired of Vue 3 development feeling sluggish with Webpack? Evan You has released Vite, a dev tool built especially for Vue 3. Before learning Vite you should understand ES Modules and HTTP/2.
1. Problem Source
1.1 Webpack Issues
When an application becomes complex, using Webpack feels uncomfortable.
<code>- Webpack Dev Server cold start time is long<br/>- Webpack HMR hot‑update response is slow<br/></code>1.2 Review Webpack Original Intent
Previously we used Webpack to bundle code into
bundle.jsfor two reasons:
<code>- Browsers did not support modular code well<br/>- Scattered module files caused many HTTP requests<br/></code>1.3 Thinking Now
The bundle becomes too large, requiring code‑splitting, compression, plugin removal, and third‑party extraction. Can the current tech stack solve Webpack’s original problems?
2. Solution Idea
2.1 ES Module
With browsers increasingly supporting the ES standard, most modern browsers now support ES Modules.
The key is using
in a
<script>tag to import/export modules.
<code>// When an ES module script is embedded in HTML, the browser requests the main.js file
// index.html
<script type="module" src="/src/main.js"></script>
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
</code>Opening
index.htmldirectly causes an error because ES modules require HTTP requests; the file protocol is not allowed.
2.2 Module Resolution
We start a static server locally and open
index.html, which reports "module vue not found" because only relative/absolute paths (starting with '/', './', '../') are valid.
<code>import vue from 'vue'
</code>In the browser, ESM cannot resolve modules from
node_modules. Normally bundlers like Webpack rewrite these imports to actual file paths. Vite’s job is to start a web server (using Koa) that proxies these modules.
<code>export function createServer(config: ServerConfig): Server {
const app = new Koa<State, Context>()
const server = resolveServer(config, app.callback())
// ... plugin setup ...
const listen = server.listen.bind(server)
server.listen = async (port: number, ...args: any[]) => {
if (optimizeDeps.auto !== false) {
await require('../optimizer').optimizeDeps(config)
}
return listen(port, ...args)
} as any
// ... other logic ...
return server
}
</code>Vite’s core is to intercept browser module requests and return processed results.
2.3 /@module/ Prefix
Comparing the original
main.jswith the one served in development shows a transformation:
<code>// original
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
</code> <code>// transformed by Vite
import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'
createApp(App).mount('#app')
</code>Vite adds the
/@module/prefix to npm packages. The rewrite logic lives in
src/node/server/serverPluginModuleRewrite.tsand works as follows:
Obtain
ctx.bodyin the Koa middleware.
Use es-module-lexer to parse the AST and collect import statements.
Determine whether an import is an absolute path (treated as an npm module).
Rewrite imports, e.g., "vue" → "/@modules/vue".
Support for
/@module/is handled in
src/node/server/serverPluginModuleResolve.ts:
Get the request path in the Koa middleware.
If it starts with
/@module/, extract the package name.
Locate the package in
node_modulesand return the appropriate entry based on its
package.json.
2.4 File Compilation
For other file types such as
.vue,
.css, and
.ts, Vite also intercepts requests and performs real‑time compilation. In Webpack we used
vue-loader; Vite does the same via its own plugins.
Original
App.vue:
<code><template>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Hello Vue 3.0 + Vite" />
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: { HelloWorld }
}
</script>
<style>
body { background: #fff; }
</style>
</code>Transformed by Vite:
<code>import HelloWorld from '/src/components/HelloWorld.vue'
const __script = {
name: 'App',
components: { HelloWorld }
}
import "/src/App.vue?type=style&index=0"
import { render as __render } from "/src/App.vue?type=template"
__script.render = __render
__script.__hmrId = "/src/App.vue"
if (typeof __VUE_HMR_RUNTIME__ !== 'undefined') __VUE_HMR_RUNTIME__.createRecord(__script.__hmrId, __script)
__script.__file = "/Users/liangxiaoying/myfile/wy-project/vite-demo/src/App.vue"
export default __script
</code>Vite splits a
.vuefile into three separate requests (script, template, style). The same on‑demand compilation applies to other file types.
2.5 HTTP 2
In HTTP 1.x, many small module files cause a large number of requests, limited by per‑domain TCP connections.
HTTP 2 introduces multiplexing, allowing all requests to share a single TCP connection.
3. Three Main Functions
Vite’s three core capabilities are Static Server, HMR, and Compile.
3.1 Fast Cold Start
When comparing
vue-cli-service serve(Webpack) with
vite serve(
npm run dev), the latter starts instantly because it only launches a static server without pre‑building the whole codebase. Vite uses esbuild (written in Go) to compile JSX, TSX, and TypeScript to native JavaScript, offering 20‑30× speed over
tsc. Caching further improves performance.
3.2 Instant Hot Module Replacement
During hot updates Vite recompiles only the changed file, resulting in a very fast response compared to Webpack, which rebuilds the entire dependency graph.
3.3 True On‑Demand Compilation
Developers can use dynamic imports (e.g.,
import('xx.js')) or Babel plugins. Unlike Webpack, which bundles every module regardless of usage, Vite relies on native ESM support and compiles files only when they are requested, greatly reducing bundle size and build time.
4. Core Idea
4.1 Initial Static Service
Running
npm run devlaunches
/src/node/server/index.ts, which creates a Koa server and a
chokidarwatcher to monitor file changes.
<code>export function createServer(config: ServerConfig): Server {
const app = new Koa<State, Context>()
const server = resolveServer(config, app.callback())
// ... plugin registration ...
const listen = server.listen.bind(server)
server.listen = async (port: number, ...args: any[]) => {
if (optimizeDeps.auto !== false) {
await require('../optimizer').optimizeDeps(config)
}
return listen(port, ...args)
} as any
// watcher setup
const watcher = chokidar.watch(root, { ignored: ['**/node_modules/**', '**/.git/**'] })
// ... other plugins (moduleRewrite, htmlRewrite, vuePlugin, cssPlugin, esbuildPlugin, etc.) ...
return server
}
</code>4.2 Listening Messages and Intercepting Requests
The first request
/vite/clientis handled by
clientPlugin(file
src/node/server/serverPluginClient.ts), which returns the client code that establishes a WebSocket connection for HMR.
<code>export const clientPublicPath = `/vite/client`
export const clientPlugin: ServerPlugin = ({ app, config }) => {
const clientCode = fs.readFileSync(clientFilePath, 'utf-8')
.replace('__MODE__', JSON.stringify(config.mode || 'development'))
app.use(async (ctx, next) => {
if (ctx.path === clientPublicPath) {
const socketPort = ctx.port
ctx.type = 'js'
ctx.status = 200
ctx.body = clientCode.replace('__HMR_PORT__', JSON.stringify(socketPort))
} else {
return next()
}
})
}
</code>The client code creates a WebSocket to receive HMR messages:
<code>const socketProtocol = __HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws')
const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`
const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')
</code>Message listener:
<code>socket.addEventListener('message', async ({ data }) => {
const payload = JSON.parse(data)
handleMessage(payload)
})
</code>Message handler (simplified):
<code>async function handleMessage(payload) {
const { path, timestamp } = payload
switch (payload.type) {
case 'connected':
console.log('[vite] connected.')
break
case 'vue-reload':
queueUpdate(import(`${path}?t=${timestamp}`).catch(err => warnFailedFetch(err, path)).then(m => () => {
__VUE_HMR_RUNTIME__.reload(path, m.default)
console.log(`[vite] ${path} reloaded.`)
}))
break
case 'vue-rerender':
const templatePath = `${path}?type=template`
import(`${templatePath}&t=${timestamp}`).then(m => {
__VUE_HMR_RUNTIME__.rerender(path, m.render)
console.log(`[vite] ${path} template updated.`)
})
break
case 'style-update':
const el = document.querySelector(`link[href*='${path}']`)
if (el) {
el.setAttribute('href', `${path}${path.includes('?') ? '&' : '?'}t=${timestamp}`)
break
}
const importQuery = path.includes('?') ? '&import' : '?import'
await import(`${path}${importQuery}&t=${timestamp}`)
console.log(`[vite] ${path} updated.`)
break
case 'style-remove':
removeStyle(payload.id)
break
case 'js-update':
queueUpdate(updateModule(path, payload.changeSrcPath, timestamp))
break
case 'custom':
const cbs = customUpdateMap.get(payload.id)
if (cbs) cbs.forEach(cb => cb(payload.customData))
break
case 'full-reload':
if (path.endsWith('.html')) {
// reload specific html page
} else {
location.reload()
}
break
}
}
</code>4.3 Plugins Listening to File Changes and Sending Messages
Example:
cssPlugin(file
src/node/server/serverPluginCss.ts) watches CSS files and sends HMR messages.
<code>export const cssPlugin: ServerPlugin = ({ root, app, watcher, resolver }) => {
app.use(async (ctx, next) => {
await next()
if (isImportRequest(ctx)) {
const { css, modules } = await processCss(root, ctx)
ctx.type = 'js'
ctx.body = codegenCss(JSON.stringify(hash_sum(ctx.path)), css, modules)
}
})
watcher.on('change', filePath => {
if (/* file is a CSS file */) {
watcher.send({
type: 'style-update',
path: publicPath,
changeSrcPath: publicPath,
timestamp: Date.now()
})
}
})
}
</code>4.4 Logical Summary
Serve the project directory as a static file server root.
Intercept specific file requests.
Resolve imports from
node_modules.
Compile Vue single‑file components (SFCs).
Implement HMR via WebSocket.
5. Snowpack VS Vite
Similarities :
Both provide a dev server based on native browser ES module imports.
Both have fast cold starts.
Both work out of the box without extensive loader/plugin configuration.
Vite supports more built‑in features (TypeScript transpilation, CSS import, CSS Modules, PostCSS) without extra installation. Snowpack also supports JSX, TypeScript, React, Preact, CSS Modules, etc., but many require separate packages.
Differences :
Plugins: Vite’s plugin ecosystem is still growing; Snowpack already offers many plugins.
Evolution: Snowpack originally lacked HMR; added in v2. Vite was inspired by Snowpack v1 and collaborated on an ESM‑HMR API.
Usage: Vite currently focuses on Vue 3 (excellent Vue integration). Snowpack is framework‑agnostic (React, Preact, Svelte, etc.).
Production bundling: Vite uses Rollup, yielding smaller bundles; Snowpack relies on Parcel or Webpack.
Bias: Vite is tightly coupled with Vue ecosystem; Snowpack is more neutral.
Documentation: Vite’s docs are improving; Snowpack’s docs are more mature.
Choosing a tool:
If you love Vue, need small bundle size, and prefer Rollup, pick Vite.
If you prefer React or other frameworks, need a rich plugin ecosystem, or want a bundler‑free dev mode, pick Snowpack.
This article aims to help Vue developers understand Evan You’s ideas on on‑demand compilation and improve development efficiency.
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.