Frontend Development 23 min read

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.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Why Vite Beats Webpack for Vue 3: Fast Startup, HMR, and On‑Demand Compilation
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.js

for 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

&lt;script&gt;

tag to import/export modules.

<code>// When an ES module script is embedded in HTML, the browser requests the main.js file
// index.html
&lt;script type="module" src="/src/main.js"&gt;&lt;/script&gt;

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')
</code>

Opening

index.html

directly 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.js

with 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.ts

and works as follows:

Obtain

ctx.body

in 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_modules

and 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>&lt;template&gt;
  &lt;img alt="Vue logo" src="./assets/logo.png" /&gt;
  &lt;HelloWorld msg="Hello Vue 3.0 + Vite" /&gt;
&lt;/template&gt;

&lt;script&gt;
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: { HelloWorld }
}
&lt;/script&gt;

&lt;style&gt;
  body { background: #fff; }
&lt;/style&gt;
</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

.vue

file 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 dev

launches

/src/node/server/index.ts

, which creates a Koa server and a

chokidar

watcher 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/client

is 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.

frontend developmentViteVue3es moduleshot module replacementOn-Demand Compilation
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.