Understanding Vite’s Core Design and Implementation: Server Startup, Pre‑bundling, and HMR
This article explores Vite’s evolution and core architecture, detailing how it improves development server startup time, performs dependency pre‑bundling with esbuild, creates the dev server, handles HTML injection, and implements fast hot‑module replacement through WebSocket communication.
This article is the first exclusive post on the Rare Earth Juejin tech community and may not be reproduced within 14 days or without authorization thereafter.
Preface
In the previous article we discussed Webpack’s design and implementation; the emergence of Vite has challenged Webpack’s dominance. Since Vite 2 (Feb 2021), Vite 3 (Jul 2022), and the recent Vite 4.0.0‑alpha.0 (Nov 2022), Vite has continuously improved the front‑end development experience. Below we explore Vite’s core design and implementation.
1. Vite claims to be the next‑generation front‑end toolchain – what problems does it solve?
1.1 Improves front‑end development server startup time
Vite initially classifies modules in the application into dependencies and source code , improving on traditional tools that start the dev server more slowly.
For dependencies, Vite uses esbuild for pre‑bundling; esbuild is written in Go and is 10‑100× faster than JavaScript‑based bundlers.
For source code (e.g., JSX, CSS, Vue/Svelte components), Vite serves the files as native ESM, letting the browser handle bundling on demand.
2. What does Vite do when it starts?
Note: the Vite source version used in this article is v3.2.4 .
We locate the entry point of the dev server in packages/vite/src/node/cli.ts and see that it creates the server by importing packages\vite\src\node\server\index.ts and calling createServer() . The following steps are performed:
...
const config = await resolveConfig(inlineConfig, 'serve', 'development')
...(1) Resolve configuration via resolveConfig , which loads plugins, the cache directory for pre‑bundled dependencies, the request‑intercepting createResolve , and the resolve field in vite.config.js (including user‑defined aliases).
...
const middlewares = connect() as Connect.Server
const httpServer = middlewareMode ? null : await resolveHttpServer(serverConfig, middlewares, httpsOptions)
const ws = createWebSocketServer(httpServer, config, httpsOptions)
...(2) Create the HTTP server, WebSocket server, and file watcher; set up code‑file watching and register middleware to handle requests for / , /js , /css , /vue , etc.
...
const container = await createPluginContainer(config, moduleGraph, watcher)
...(3) Create the plugin container that holds all registered plugins.
...
const server: ViteDevServer = { ... }
server.transformIndexHtml = createDevHtmlTransformFn(server)
...(4) Transform HTML files, injecting the client script <script type="module" src="/@vite/client"></script> that loads the Vite client.
(5) Before the server starts, Vite calls container.buildStart({}) to trigger each plugin’s buildStart hook, then runs initDepsOptimizer() to pre‑bundle dependencies.
3. Analyzing Vite’s underlying principles
3.1 Why does Vite perform pre‑bundling?
Pre‑bundling is a trade‑off between ultra‑fast server start‑up and first‑screen rendering speed. Vite leaves user source files untouched, serving them as ESM on demand, while it pre‑bundles third‑party dependencies to reduce HTTP requests.
The pre‑bundling process includes:
(1) Cache check: Vite reads node_modules\.vite\_metadata.json to see if a previous cache exists and whether the hash matches.
(2) Dependency scanning via discoverProjectDependencies() , collecting entry‑point dependencies and applying the preAliasPlugin for alias support.
(3) Building dependencies with esbuildDepPlugin() , converting non‑ESM code to ESM and bundling multiple files into a single cacheable file, dramatically reducing HTTP requests.
(4) Writing the result to _metadata.json for future runs.
{
"hash": "1a547ddf",
"browserHash": "2065b8ab",
"optimized": {
"@vue/runtime-core": {
"file": "E:/codeWorlk/xxx/node_modules/.vite/@vue_runtime-core.js",
"src": "E:/codeWorlk/xxx/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js",
"needsInterop": false
},
"vue": {
"file": "E:/codeWorlk/xxx/node_modules/.vite/vue.js",
"src": "E:/codeWorlk/xxx/node_modules/vue/dist/vue.runtime.esm-bundler.js",
"needsInterop": false
},
"vue-router": {
"file": "E:/codeWorlk/xxx/node_modules/.vite/vue-router.js",
"src": "E:/codeWorlk/xxx/node_modules/vue-router/dist/vue-router.esm-bundler.js",
"needsInterop": false
},
"@vue/reactivity": {
"file": "E:/codeWorlk/xxx/node_modules/.vite/@vue_reactivity.js",
"src": "E:/codeWorlk/xxx/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js",
"needsInterop": false
}
}
}Only npm dependencies are pre‑bundled; user‑written files are served directly as ESM, so a large number of user files without code‑splitting can still cause slow first‑screen loads.
3.2 How does a request reach the Vite server?
<script type="module" src="/src/main.js"></script>
// or
import { get } from './utils'Requests are processed by the transformMiddleware(server) function, which parses, loads, and transforms modules using the plugin container.
// main transform middleware
middlewares.use(transformMiddleware(server))The middleware workflow is:
Determine if the incoming request should be handled.
Use transformRequest() to parse, load, and transform the request.
If the module already exists in the graph, return it directly.
Otherwise, invoke the appropriate plugin’s resolve hook, create a module entry, and store it.
Finally, call the esbuildPlugin ’s transform hook, cache the compiled code, and return it.
(3) How does Vite achieve fast Hot Module Replacement (HMR)?
HMR works by creating a WebSocket server ( const ws = createWebSocketServer(...) ) that watches file changes. When a file changes, the watcher triggers:
watcher.on('change', async (file) => {
file = normalizePath(file)
if (file.endsWith('/package.json')) {
return invalidatePackageData(packageCache, file)
}
// invalidate module graph cache on file change
moduleGraph.onFileChange(file)
if (serverConfig.hmr !== false) {
try {
await handleHMRUpdate(file, server)
} catch (err) {
ws.send({
type: 'error',
err: prepareError(err)
})
}
}
})If a change is detected, handleHMRUpdate() rebundles the affected module and notifies the client, which then reloads the updated code.
Conclusion
Vite 4.0.0 continues to evolve, offering an increasingly friendly experience for front‑end developers. This article skimmed the core startup flow; further topics such as Vite’s plugin system and file‑scanning mechanisms are worth exploring.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
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.