Why Vite Beats Webpack: Deep Dive into Architecture and HMR
This article examines the limitations of bundle‑based tools like Webpack, introduces Vite’s ESM‑based approach, and provides a detailed analysis of Vite’s architecture, hot‑module‑replacement, module transformation, dependency graph, and plugin system, illustrating how it improves development speed and efficiency.
1. Background
Webpack is currently the most popular bundle‑based build tool. As a project grows, build time becomes longer, severely affecting the development experience. The reason is that all modules must be bundled into a single bundle, even dynamically imported ones, so developers must wait for the entire project to be built regardless of how many dependencies the entry page actually uses.
Figure 1: Bundle‑based development server
2. Vite Introduction
To solve the above problems, Vite was created. Vite leverages the browser’s native support for JavaScript modules (ESM) and moves the build process from bundle‑level to module‑level, using ESM as the granularity of the build.
Figure 2: Native ESM development server
2.1 Problems with ESM‑Based Builds
Performance overhead of converting to ESM
Compatibility issues with non‑ESM modules (TS/JSX, etc.)
Node modules incompatibility
Many dependencies (e.g., lodash) cause a large number of requests
2.2 How Vite Addresses These Issues
Uses esbuild to transform TS/JSX
Wraps CJS/UMD files and converts them to ESM
Figure 3: CJS to ESM conversion
Packages Node modules into a single module to reduce request count
2.3 Other Effective Techniques
Cache ESM modules in node_modules/.vite Hot Module Replacement (HMR)
HTTP caching
Runtime dynamic dependency scanning
3. Vite Architecture and Source‑Code Analysis
Vite fully exploits browser features and the Node toolchain, using a Node server that communicates with the browser to map source files to the UI efficiently.
Figure 4: Node server processing flow
Figure 5: Development environment runtime flow
3.2.1 Hot Update Code
function updateModules(
file: string,
modules: ModuleNode[],
timestamp: number,
{ config, ws }: ViteDevServer
) {
const updates: Update[] = []
const invalidatedModules = new Set<ModuleNode>()
let needFullReload = false
for (const mod of modules) {
invalidate(mod, timestamp, invalidatedModules)
if (needFullReload) {
continue
}
const boundaries = new Set<{ boundary: ModuleNode; acceptedVia: ModuleNode }>()
const hasDeadEnd = propagateUpdate(mod, boundaries)
if (hasDeadEnd) {
needFullReload = true
continue
}
updates.push(
...[...boundaries].map(({ boundary, acceptedVia }) => ({
type: `${boundary.type}-update` as Update['type'],
timestamp,
path: boundary.url,
acceptedPath: acceptedVia.url
}))
)
}
if (needFullReload) {
config.logger.info(colors.green(`page reload `) + colors.dim(file), {
clear: true,
timestamp: true
})
ws.send({ type: 'full-reload' })
} else {
config.logger.info(
updates.map(({ path }) => colors.green(`hmr update `) + colors.dim(path)).join('
'),
{ clear: true, timestamp: true }
)
ws.send({ type: 'update', updates })
}
}3.2.2 Boundary Propagation
function propagateUpdate(
node: ModuleNode,
boundaries: Set<{ boundary: ModuleNode; acceptedVia: ModuleNode }>,
currentChain: ModuleNode[] = [node]
): boolean /* hasDeadEnd */ {
if (node.isSelfAccepting) {
boundaries.add({ boundary: node, acceptedVia: node })
for (const importer of node.importers) {
if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
propagateUpdate(importer, boundaries, currentChain.concat(importer))
}
}
return false
}
if (!node.importers.size) {
return true
}
if (!isCSSRequest(node.url) && [...node.importers].every(i => isCSSRequest(i.url))) {
return true
}
for (const importer of node.importers) {
const subChain = currentChain.concat(importer)
if (importer.acceptedHmrDeps.has(node)) {
boundaries.add({ boundary: importer, acceptedVia: node })
continue
}
if (currentChain.includes(importer)) {
return true
}
if (propagateUpdate(importer, boundaries, subChain)) {
return true
}
}
return false
}3.2.3 Transform Middleware
// transformMiddleware
if (
isJSRequest(url) ||
isImportRequest(url) ||
isCSSRequest(url) ||
isHTMLProxy(url)
) {
// strip ?import
url = removeImportQuery(url)
// unwrap virtual ids
url = unwrapId(url)
if (isCSSRequest(url) && !isDirectRequest(url) && req.headers.accept?.includes('text/css')) {
url = injectQuery(url, 'direct')
}
const ifNoneMatch = req.headers['if-none-match']
if (ifNoneMatch && (await moduleGraph.getModuleByUrl(url, false))?.transformResult?.etag === ifNoneMatch) {
res.statusCode = 304
return res.end()
}
const result = await transformRequest(url, server, {
html: req.headers.accept?.includes('text/html')
})
if (result) {
const type = isDirectCSSRequest(url) ? 'css' : 'js'
const isDep = DEP_VERSION_RE.test(url) || (cacheDirPrefix && url.startsWith(cacheDirPrefix))
return send(req, res, result.code, type, {
etag: result.etag,
cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache',
headers: server.config.server.headers,
map: result.map
})
}
}3.2.4 Module Graph Update
/**
* Update the module graph based on a module's updated imports information
* If there are dependencies that no longer have any importers, they are
* returned as a Set.
*/
async function updateModuleInfo(
mod: ModuleNode,
importedModules: Set<string | ModuleNode>,
acceptedModules: Set<string | ModuleNode>,
isSelfAccepting: boolean,
ssr?: boolean
): Promise<Set<ModuleNode> | undefined> {
mod.isSelfAccepting = isSelfAccepting
const prevImports = mod.importedModules
const nextImports = (mod.importedModules = new Set())
let noLongerImported: Set<ModuleNode> | undefined
for (const imported of importedModules) {
const dep = typeof imported === 'string' ? await this.ensureEntryFromUrl(imported, ssr) : imported
dep.importers.add(mod)
nextImports.add(dep)
}
prevImports.forEach(dep => {
if (!nextImports.has(dep)) {
dep.importers.delete(mod)
if (!dep.importers.size) {
;(noLongerImported || (noLongerImported = new Set())).add(dep)
}
}
})
const deps = (mod.acceptedHmrDeps = new Set())
for (const accepted of acceptedModules) {
const dep = typeof accepted === 'string' ? await this.ensureEntryFromUrl(accepted, ssr) : accepted
deps.add(dep)
}
return noLongerImported
}
async function ensureEntryFromUrl(rawUrl: string, ssr?: boolean): Promise<ModuleNode> {
const [url, resolvedId, meta] = await this.resolveUrl(rawUrl, ssr)
let mod = this.urlToModuleMap.get(url)
if (!mod) {
mod = new ModuleNode(url)
if (meta) mod.meta = meta
this.urlToModuleMap.set(url, mod)
mod.id = resolvedId
this.idToModuleMap.set(resolvedId, mod)
const file = (mod.file = cleanUrl(resolvedId))
let fileMappedModules = this.fileToModulesMap.get(file)
if (!fileMappedModules) {
fileMappedModules = new Set()
this.fileToModulesMap.set(file, fileMappedModules)
}
fileMappedModules.add(mod)
}
return mod
}3.2.5 Import Analysis Plugin
/**
* Server‑only plugin that lexes, resolves, rewrites and analyzes url imports.
* - Imports are resolved to ensure they exist on disk
* - Lexes HMR accept calls and updates import relationships in the module graph
* - Bare module imports are resolved (by @rollup‑plugin/node‑resolve) to absolute file paths
* - CSS imports are appended with `.js` so that both the JS module and the actual CSS go through the transform pipeline
*/
export function importAnalysisPlugin() {
// implementation omitted for brevity
}
/**
* Detect import statements to a known optimized CJS dependency and provide ES named imports interop.
*/
export function transformCjsImport() {
// implementation omitted for brevity
}4. Conclusion
Vite can greatly improve development efficiency, but its ecosystem is still maturing and requires extensive community testing. In practice, Vite is recommended for development, while Webpack remains the preferred choice for production builds. The current React refresh‑plugin is based on Babel and has performance drawbacks; future improvements should focus on native solutions to reduce inefficient JavaScript overhead. For deeper source‑code analysis, use Node and Chrome debugging tools.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Sensors Frontend
Regularly shares the Sensors tech team's cutting‑edge explorations and technical insights in front‑end development.
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.
