深夜十一点,加班中的前端工程师阿伟点了一份番茄鸡蛋面。他走进第一家“中央厨房餐厅”,服务员却抱歉地说:先生,我们正在备菜,要先把菜单上 108 道菜全部做好,预计 40 分钟后才能给您上第一道菜。阿伟懵了:我只点一碗面啊!这家餐厅,就是 Webpack 的缩影——启动前把整个项目的几千个文件全部打包好,无论你当下只需要哪一个页面。 阿伟转身走进隔壁的“即时厨房餐厅”,刚坐下翻菜单,面已经端上来了。这里的后厨坚持一个原则:你点什么,我才开火做什么。更妙的是,这家店已经提前把常用调料做成半成品,你说“多放辣”,传菜员只把你这碗面端回去重做,同桌其他菜纹丝不动。这家餐厅,就是 Vite。 Vite 为什么快?因为它把“备菜”和“上菜”彻底解耦:依赖浏览器原生的 ESM 模块加载能力按需编译,用 esbuild 预构建第三方依赖,再用 HMR 只替换改动的那个模块。读完这篇,你会像阿伟一样,看清这套“现点现做”的加速魔法背后每一环是怎么协作的。
对 AI 从业者而言,理解 Vite 的加速原理意味着你能在大型前端工程中快速定位构建瓶颈,把开发反馈从“分钟级”压到“毫秒级”,让 AI 生成代码→验证效果→修正的循环效率直接翻倍。
目录
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
开发痛点:全量打包之痛
深夜加班,阿伟走进“中央厨房餐厅”,点了一碗番茄鸡蛋面。服务员满脸歉意:先生,后厨正在备菜,得先把菜单上 108 道菜全部做熟,预计 40 分钟后才轮到您这一碗。阿伟瞪大眼睛:我只点了一道菜啊!这正是 Webpack 的日常——不管开发者在浏览器里打开哪个页面,构建工具都要先把整个项目的所有模块从入口开始递归扫描、转换、打包成一个巨大的 bundle 文件,然后开发服务器才能启动。
这个“备菜”过程就是冷启动。项目的模块数量与打包耗时呈线性关系:一个几千模块的中大型项目,Webpack 冷启动 30 秒以上是家常便饭。更折磨人的是迭代阶段:修改一行组件的样式,watch 模式虽然不用重新处理 node_modules,但仍要重新构建受影响的 chunk、重新计算 hash,等 2 到 10 秒才能看到效果。高频的前端开发中,这些等待累积起来,一天要浪费掉大把时间。
用一个简单的代价模型来量化这件事:全量打包的总成本,等于项目里每一个模块的转换耗时相加。
中央厨房的逻辑是:不管客人点什么,后厨都要先做完 108 道菜。Webpack 时代的痛点本质上是把“开发服务器”错当成了“打包服务器”——全量成本由菜单规模决定,和你真正需要的那道菜毫无关系。
- 总代价符号$C_{\text{total}}$ 表示一次完整打包消耗的总时间,是后面所有对比的“分子”。
- 累加求和$\sum_{i=1}^{n}$ 像流水线上逐个工位累计工时,把所有模块的编译时间一项项加起来。
- 单个模块代价$c(m_i)$ 指第 $i$ 个模块经过 loader 转换(如 TS 转 JS、Vue 编译)所花的时间。
- 模块总数$n$ 是项目模块总数,模块越多,这个求和的结果就越大。
原生ESM:浏览器的点菜通道
阿伟好奇地问:难道没有一家餐厅能现点现做吗?还真有。这类“即时厨房餐厅”的每张餐桌上都有一个传菜铃,客人按铃,后厨对应档口立刻开火。浏览器自己就带了这套传菜铃——原生 ES Module(ESM)。自 ES6 起,JavaScript 有了正式模块标准:export 把变量交给外部,import 从其他文件引入能力。
要用上这条通道,只需在 HTML 里写 <script type="module">。浏览器解析到这个标签时,会把脚本当作 ES 模块处理:扫描 import 声明,然后按模块路径发起 HTTP GET 请求。换句话说,加载模块这件事浏览器自己就能完成,根本不需要一个打包器提前把所有模块塞进一个大文件里。
这里的关键性质是:import 是静态声明,而不是运行时的条件判断。浏览器在解析阶段就能看清“入口依赖了谁、谁又依赖了谁”,于是会并发地发起模块请求。这种“静态可分析”的特点,正是后面 Vite 能做按需加载的基石——浏览器天然知道要什么,Vite 只需要当好传菜员。
传菜铃一响,后厨的对应档口直接开火——阿伟按下的每一次 import,对应浏览器发出的一次 GET 请求。没有中央厨房的中间装配,菜自然上得又快又新鲜。
import 对应一个请求,模块的依赖关系由浏览器自己递归展开。- 导入语句
\text{import } x \text{ from } u是 ESM 的标准写法,$x$ 是导入的名字,$u$ 是模块标识符。 - 语义蕴含长箭头 $\Longrightarrow$ 读作“将会触发”,表达从源码声明到网络行为的因果映射。
- 网络请求$\text{HTTP GET: } u$ 表示浏览器直接去服务器要 $u$ 这个文件,拿到后按模块执行。
按需加载:请求驱动编译
阿伟翻开菜单,还没决定吃什么,番茄鸡蛋面居然先上来了。服务员笑着解释:这就是即时厨房的规矩——您刚打开菜单,后厨就只把第一页需要的食材准备好了,其他页的菜连洗都没洗。Vite 干的正是这件事。
Vite 开发服务器启动时几乎不碰业务代码,它只是把 dev server 跑起来等着。浏览器请求 index.html,拿到一个极轻量的 HTML,里面引用 /src/main.ts;浏览器去请求它,Vite 才用 esbuild 和插件把这个文件即时转换成浏览器能直接执行的 ESM 返回;这个模块里的 import 又被浏览器自动请求……如此循环。整个过程是“请求驱动编译”:没有被请求的模块,编译器碰都不碰。这就解释了为什么项目再大,Vite 冷启动也能保持在秒级——它根本不去看那些用不到的路由页面。
把两家的冷启动时间放在一起对比,差距一目了然:
你点番茄炒蛋,后厨就只洗番茄、打鸡蛋;你点回锅肉,才另开一口锅。Vite 的哲学很简单——不为没点的菜付费,不为没请求的模块编译。
- Webpack 全量成本$T_{\text{webpack}}$ 是冷启动总耗时,遍历 $M_{\text{all}}$ 全部模块。
- Vite 按需成本$T_{\text{vite}}$ 是冷启动总耗时,只遍历可达集合 $\mathcal{R}$ 中的模块。
- 可达集合$\mathcal{R}(\text{entry})$ 表示从入口出发,沿着
import边能走到的全部模块——对应浏览器真正发出请求的那些文件。 - 单模块转换$c(m)$ 是转换一个模块的代价,两种模式都按模块计费,区别只在“计费的模块数量”。
依赖预构建:备好半成品调料
即时厨房也有烦恼:有些“菜”的预处理特别慢。比如泡发干香菇要两小时(CommonJS 老格式依赖),再比如一箱菜心要摘八百根(lodash 这种包含几百个小文件的库)。如果客人每次点菜都现场从头处理,那和中央厨房也没什么区别。所以这家店的诀窍是:每天凌晨把常用调料统一做成半成品,客人点菜时直接下锅。
Vite 的“凌晨备菜”叫依赖预构建。dev server 启动时,Vite 会扫描源码里的 import 语句,找出所有来自 node_modules 的裸导入(比如 import React from 'react'),然后用 esbuild 把它们统一预构建成标准的 ESM 文件,缓存到 node_modules/.vite 目录。这一步同时解决三个问题:一是兼容,node_modules 里有大量 CommonJS 和 UMD 格式的包,浏览器原生 ESM 无法直接导入,必须转换;二是请求数,lodash 几百个内部小模块要是让浏览器逐个请求,性能直接崩溃,预构建把它们合并成单个文件;三是稳定性,预构建完的依赖不会因为源码改动而重新转换,天然适合缓存。
半成品调料备好了,客人点菜时后厨几分钟就能出餐。Vite 也因此不用在每一个源码模块里去重新解析庞大的框架依赖——直接把预构建后的标准模块发给浏览器,省掉的是最粗重的重复劳动。
- 全称量词$\forall d \in D_{\text{dep}}$ 表示“对于依赖集合里的每一个包”,一个都不落下。
- 依赖集合$D_{\text{dep}}$ 是从源码的裸导入中收集到的第三方包集合,即
react、lodash这些 node_modules 里的库。 - 格式转换$\mathrm{cjs}(d)\xrightarrow{\text{esbuild}}\mathrm{esm}(d)$ 表示把包 $d$ 从 CommonJS 格式经过 esbuild 处理后输出为标准 ESM。
Webpack对比:两种餐厨模式
中央厨房真的一无是处吗?也不是。它适合大型宴会——批量出菜稳定、流程可控;而即时厨房依赖客人主动点菜,万一同时有几百桌客人各点几百道菜,传菜效率就成灾难。Vite 在开发阶段用即时厨房模式爽快加速,到了生产构建阶段,仍然要用 Rollup 把成百上千个模块打包好——浏览器面对几百个文件请求,可比后厨面对几百份菜单还要头疼。
对比两种模式,差异集中在四个维度。打包时机上,Webpack 启动前全量打包,Vite 请求时按需编译。启动耗时上,Webpack 与项目总规模线性相关,常以十秒计;Vite 只关心首屏模块,秒级启动。更新粒度上,Webpack 修改后按 chunk 重建,Vite 精确到被修改的单个模块。而生产构建环节,Webpack 用自家 loader/plugin 链路,Vite 则交给 Rollup(tree-shaking、代码分割、产物优化更成熟)。所以“Vite 比 Webpack 快”这句话只在开发期严格成立,生产构建两者各有取舍。
选中央厨房还是即时厨房,取决于场景。开发期需要高频迭代反馈,即时厨房完胜;生产期要稳定可控的产物,两家都得“打包上桌”。Vite 的聪明之处在于,把宝贵的“快”全部留给了开发者每天面对的开发环节。
- 冷启动时间$T_{\text{vite}}^{\text{cold}}$ 和 $T_{\text{webpack}}^{\text{cold}}$ 分别是 Vite 与 Webpack 的冷启动耗时上标 cold 表示“第一次从零开始”。
- 远小于符号$\ll$ 表示左侧比右侧小一个数量级以上,是量级上的碾压而非细微差距。
- 对比对象$T_{\text{webpack}}^{\text{cold}}$ 是 Webpack 启动前全量打包的耗时,包含了遍历千个模块的代价。
HMR原理:只重做一道菜
阿伟尝了一口面,对服务员说:“麻烦多加一点辣。”在传统“刷新式餐厅”,后厨会把这桌菜全部倒掉重做一遍;但在即时厨房,传菜员拿出对讲机:“只重做阿伟那碗面。”对讲机就是 WebSocket,这碗面就是被修改的模块。HMR——Hot Module Replacement,热模块替换——做的是同一件事:页面不刷新,只替换该换的那一个模块。
完整链路是这样跑的:文件保存后,Vite 的文件监听器捕捉到变化;dev server 判断该文件是否正被浏览器使用,若是,就通过 WebSocket 推一条消息:“模块 /src/App.vue 已失效”;浏览器收到后,重新发起 HTTP 请求获取这个模块的编译产物;接着 Vite 的 HMR runtime 沿着模块依赖图检查边界——如果模块自己调用了 import.meta.hot.accept,就只替换它自己;如果没声明接受,就把失效状态向上冒泡,直到找到声明接受热更新的祖先;一路冒泡到根部还没有,才退化为整页刷新。整个过程浏览器只重新执行了一个模块的代码,组件状态(比如 React 的 useState)自然就保留了下来。
从成本模型看,热替换和全量刷新的差距是结构性的:
HMR 的哲学就是“处理最小变更”。后厨不需要重新热一桌菜,只把那道需要调整的菜端回锅——阿伟同桌的汤和甜点,连温度都没有变过。
- 热更新代价$C_{\text{ws}}$ 是 WebSocket 传递“模块失效”消息的开销,几毫秒;
- 重新请求模块$C_{\text{req}}(m)$ 是浏览器重新 GET 模块 $m$ 的网络耗时;
- 模块替换执行$C_{\text{swap}}(m)$ 是 HMR runtime 执行模块替换、刷新依赖引用的前端代价;
- 全量刷新代价$C_{\text{rebuild}}+C_{\text{reinit}}$ 指重新构建受影响 chunk、并整页重新初始化应用的总成本。
缓存优化:记住客人口味
即时厨房还有一个终极武器:记忆。阿伟第二次光顾,刚坐下,番茄鸡蛋面已经上桌了——后厨记得他的口味,省掉了全部重新确认的流程。Vite 的缓存策略也分两层:记住“调料”(第三方依赖),记住“菜”(源码)。
第一层针对预构建的依赖产物,使用 Cache-Control: max-age=31536000, immutable 强缓存。这些文件内容几乎不变,且文件名带内容 hash——内容变了 hash 变了,URL 就完全不同,浏览器直接命中本地缓存,连请求都不会发出。第二层针对源码模块,使用协商缓存:浏览器请求源码时带上 If-None-Match(ETag),Vite 比较文件内容是否变化;没变就返回 304 空响应体,变了才重新编译并返回 200 新内容。另外,预构建产物本身还缓存在磁盘上的 node_modules/.vite,重启 dev server 时如果依赖没变会直接复用,省掉重新 esbuild 的时间。
用加权平均来理解缓存的价值:
记忆让第二次点单近乎零成本,缓存让第二次加载近乎零成本。Vite 把“不变的东西”全部记住,把算力只留给真正变化的部分——这正是它越用越快的秘密。
- 平均加载时间$T'$ 表示一个模块多次加载的平均耗时,是衡量缓存效果的指标;
- 加权结构$p_{\text{hit}} \times T_{\text{cache}}$ 是命中缓存那部分请求的成本,$(1-p_{\text{hit}})$ 是未命中比例;
- 未命中代价未命中时要先读缓存再做编译,所以括号内是 $T_{\text{cache}}+T_{\text{compile}}$;
- 极限情况当 $p_{\text{hit}}=1$ 时,$T'=T_{\text{cache}}\approx 0$,加载成本趋近于零。
AI 应用场景
大型中后台项目的开发提效
阶段:开发工具链优化(编码期反馈回路)
解决的问题:传统 Webpack 模式下,一个 2000+ 模块的中后台管理系统冷启动要 40 秒,每次改动等 5 秒以上,严重影响“改代码→看效果”的反馈循环。迁移 Vite 后,冷启动压到 1 秒内,HMR 毫秒级响应,开发者的心流不被打断。
具体案例:某电商运营后台的前端团队将构建工具从 Webpack 迁移到 Vite。迁移前,产品经理提一个筛选器交互优化,前端改完代码要等 8 秒才能验证;迁移后,保存文件几乎瞬间看到效果,人均每日节省近 40 分钟等待时间。这 40 分钟被投入到更重要的代码评审和性能优化中。
AI 辅助编程的快速验证闭环
阶段:AI 生成代码后的验证与修正阶段
解决的问题:开发者用 Copilot 或大模型生成组件代码时,经常需要“生成→运行→看效果→修改提示词”反复迭代。如果构建工具反馈慢,这个闭环就被几秒甚至几十秒的等待打断。Vite 的秒级启动和毫秒级 HMR,让 AI 生成的每一次修改都能立即被验证。
具体案例:一位数据可视化工程师用 AI 生成 ECharts 配置,每轮修改后页面立即热更新,5 分钟内完成了从生成到调优的完整循环。同样的任务在旧工具链下需要 20 分钟,因为大部分时间花在等待构建上。
微前端与 Monorepo 架构下的开发体验
阶段:大型工程的基础设施建设
解决的问题:Monorepo 中几十个子应用共享依赖,全量打包会让冷启动时间随仓库体积膨胀。Vite 的依赖预构建 + 按需加载让每个子应用只编译自己需要的模块,配合 Nx/Turborepo 的任务缓存,大型工程的开发体验接近小型项目。
具体案例:某金融科技公司的 Monorepo 中,数据中台、风控后台、报表系统共享设计系统组件。引入 Vite 后,各子应用开发环境独立启动,公共依赖提前预构建缓存,单个子应用冷启动稳定在 800ms 以内。
Python 演示
import random class Module: """模拟前端工程中的一个源文件模块""" def __init__(self, name, deps=None, compile_ms=None): self.name = name self.deps = deps or [] self.compile_ms = compile_ms if compile_ms is not None else random.uniform(5.0, 15.0) def build_all(modules): """模拟 Webpack: 冷启动时全量打包所有模块""" return sum(m.compile_ms for m in modules.values()) def load_entry(entry, modules): """模拟 Vite: 只编译从入口可达的模块(按需加载)""" seen = set() def visit(name): if name in seen: return 0.0 seen.add(name) cost = modules[name].compile_ms for dep in modules[name].deps: cost += visit(dep) return cost return visit(entry), seen def patch_one(target): """模拟 HMR: 只重新编译被修改的那个模块""" return target.compile_ms if __name__ == "__main__": random.seed(2024) # 构造 100 个模块: 入口 m0 只依赖 6 个业务模块, 其余模块属于"未访问路由" mods = {} mods["m0"] = Module("m0", ["m1", "m2"]) mods["m1"] = Module("m1", ["m3", "m4"]) mods["m2"] = Module("m2", ["m5", "m6"]) for i in range(3, 100): mods["m%d" % i] = Module("m%d" % i) cold_all = build_all(mods) cold_demand, used = load_entry("m0", mods) changed = mods["m1"] update_webpack = build_all(mods) update_hmr = patch_one(changed) print("冷启动耗时 Webpack=%.1fms Vite=%.1fms 加速 %.1fx" % (cold_all, cold_demand, cold_all / cold_demand)) print("改一行耗时 Webpack=%.1fms HMR=%.1fms 加速 %.1fx" % (update_webpack, update_hmr, update_webpack / update_hmr)) print("Vite 冷启动只编译 %d 个可达模块 / 全项目 %d 个" % (len(used), len(mods)))
self.compile_ms = compile_ms if compile_ms is not None else random.uniform(5.0, 15.0)给每个模块分配一个随机的编译耗时,模拟真实工程中不同文件的转换成本。return sum(m.compile_ms for m in modules.values())Webpack 模式把 100 个模块的编译时间全部累加,体现“全量打包”。def visit(name):从入口开始递归遍历依赖图,模拟浏览器沿着 import 边按需请求模块。if name in seen:已经编译过的模块直接跳过,避免循环依赖和重复编译,这也是浏览器加载 ESM 的去重逻辑。for dep in modules[name].deps:遍历当前模块的依赖列表,相当于解析源码中的 import 声明。patch_one(changed)HMR 只看被修改的那一个模块,不关心其他 99 个模块是否发生变化。
常见误区
误区我一直以为 Vite 之所以快,是因为它用 esbuild 替代了 Webpack 的打包器。既然如此,把 esbuild 塞进 Webpack 里不也能达到同样效果吗?
正解Vite 开发期快的最核心原因是“不打包”——利用浏览器原生 ESM 按需加载,只编译被请求的模块。esbuild 只负责两件事:依赖预构建和单文件转换,它并没有把整个项目打包成一个 bundle。就像即时厨房快是因为“现点现做”,而不是因为厨师手速快。把 esbuild 塞进 Webpack 只能让全量打包变快一点,但“全量”这个结构性瓶颈依然存在。
为何常见:人们习惯把“快”归因到一个具体工具上,而忽略了架构层面的差异。esbuild 是“备菜更快的手段”,但 Vite 的模式是“根本不需要备满汉全席”。
误区我以为 HMR 就是自动刷新页面——文件保存后浏览器自动 reload,不用自己按 F5。以前用 Webpack 时也是这么回事。
正解页面自动刷新(full reload)恰恰是 HMR 失败时的兜底方案。真正的 HMR 能做到“页面不刷新、状态不丢失”:改了一个 React 组件,组件的 useState 数据还在,输入框内容还在,只是组件的渲染函数被替换执行了。这就像后厨只重做你那一碗面,同桌的汤和甜点纹丝不动;而自动刷新等于把整桌菜全部倒掉重上。
为何常见:很多脚手架默认配置下,用户改动文件后页面确实会自动更新,于是大家把“自动刷新”误当成 HMR。只有当修改的模块声明了 import.meta.hot.accept 时才触发真正的热替换,否则就会退化成整页刷新。
误区开发模式 Vite 这么快,生产构建肯定也完胜 Webpack。我准备把公司的生产构建流程也直接换成 Vite,构建时间一定能大幅缩短。
正解Vite 的“快”主要体现在开发模式。生产构建时 Vite 默认使用 Rollup 而不是 esbuild——因为 Rollup 的 tree-shaking、代码分割、产物优化生态更成熟,适合产出面向所有浏览器的稳定包。生产构建的耗时不一定比 Webpack 快,有时甚至更慢。开发模式和生产模式是两套完全不同的引擎,就像即时厨房的外卖很快,但要筹办 500 人的宴会,还是得用中央厨房的流水线。
为何常见:产品宣传的“快”通常用开发体验截图,用户容易把开发期的优势外推到生产期。
误区Vite 是尤雨溪做的,肯定只能配 Vue 用。我是 React 技术栈,所以 Vite 跟我没关系。
正解Vite 是框架无关的构建工具,官方支持 React、Vue、Svelte、Solid、Vanilla TS 等,通过插件机制接入各框架的编译能力。Vite 核心里没有一行代码依赖 Vue;它只负责模块加载、依赖预构建和 HMR 基础设施,框架的 JSX/模板编译全部交给对应插件。就像即时厨房的“传菜系统”不挑菜系——川菜、粤菜、西餐都能用同一套传菜铃。
为何常见:Vite 的作者尤雨溪同时也是 Vue 的作者,这种身份绑定让很多人误以为工具链也被“绑定”了。
误区既然浏览器原生支持 `import`,那 Vite 开发模式应该零编译,直接把源码发给浏览器就行了吧?
正解浏览器只是能加载标准 ESM 文件,但你的源代码往往是 TS、JSX、Vue SFC 这些浏览器完全不认识的格式。Vite 仍然要用 esbuild 把 .ts 转成 JS、把 JSX 转成 React.createElement、把 .vue 编译成 render 函数——只是它只对“被请求的那个文件”做转换,而不是对整个项目做转换。传菜铃代替不了厨师,只是让厨师只为这一道菜开火。
为何常见:把“原生 ESM 按需加载”误解为“零编译”,忽略了开发源码与现代浏览器执行格式之间的鸿沟。
面试题
Q1. Vite 开发模式冷启动快的核心原因是什么?
- A.esbuild 能高速打包项目全部代码
- B.原生ESM按需加载只编译请求模块← 正确答案
- C.WebSocket 实现了预编译缓存
- D.浏览器开启了 HTTP/2 压缩
答案:B。Vite 的核心加速点在于“不打包”,利用浏览器原生 ESM,浏览器请求哪个模块 Vite 才编译哪个模块。esbuild 只用于依赖预构建和单文件转换,不是全量打包;WebSocket 服务于 HMR 的更新通知,而不是冷启动;HTTP/2 只是传输层优化。就像即时厨房快是因为“现点现做”,而不是传菜铃更快。
Q2. Vite 依赖预构建主要解决什么问题?
- A.CommonJS 转 ESM 和减少请求数← 正确答案
- B.压缩源码体积加快页面加载
- C.把全部业务代码合并成单文件
- D.自动修复项目中所有语法错误
答案:A。依赖预构建用 esbuild 把 node_modules 里的 CommonJS/UMD 包统一转换为标准 ESM,同时把像 lodash 这种由几百个小文件组成的依赖合并成单个文件,减少浏览器请求数量。它不做源码压缩,不合并业务代码,更不会修复语法错误——就像提前备好的“半成品调料”解决的是下锅速度问题,而不是菜的味道问题。
Q3. <script type="module"> 在浏览器中的行为是?
- A.让脚本在解析完成后同步执行
- B.标记为ES模块并支持 import 按需加载← 正确答案
- C.仅用于生产环境的代码打包
- D.自动将全部依赖合并交付
答案:B。type="module" 告诉浏览器把脚本当作 ES 模块处理,脚本可以包含 import/export,浏览器会按需发起 HTTP 请求加载依赖,并默认延迟执行(defer)。它不合并依赖、不只用于生产、也不是同步执行。这相当于按下传菜铃后,浏览器自己知道去哪几口锅取菜。
Q4. 修改一行代码后 Vite HMR 的第一步是什么?
- A.浏览器刷新整个页面重新加载
- B.服务器打包全部模块并推送
- C.服务器通过WebSocket推送更新← 正确答案
- D.浏览器主动向服务器轮询变更
答案:C。文件保存后,Vite dev server 的文件监听器捕捉到变化,通过 WebSocket 主动向浏览器推送“某模块已失效”的消息。浏览器收到后再重新请求该模块。A 是 HMR 冒泡失败的兜底方案,B 是 Webpack 整包重建的思路,D 把推送模式误解成了轮询模式——传菜员用对讲机通知,而不是后厨盯着客人看。
Q5. 模块未调用 import.meta.hot.accept 时 HMR 会?
- A.直接替换并通知所有父模块
- B.向上冒泡找接受者否则刷新← 正确答案
- C.忽略修改保留旧模块运行
- D.立即终止开发服务器运行
答案:B。HMR 遵循“依赖边界”规则。当被修改模块自己没有调用 accept 接受热更新时,失效状态会向上冒泡,沿依赖图寻找最近的声明了 accept 的父模块;如果冒泡到入口都没找到,就整页刷新作为兜底。这就像传菜员重做一道菜,如果发现那盘菜牵扯到整桌菜谱的结构,只能整桌重上。
Q6. Vite 的缓存策略正确的有哪些?
- A.依赖预构建产物使用强缓存← 正确答案
- B.源码文件使用协商缓存 304← 正确答案
- C.所有模块统一使用强缓存
- D.缓存失效时必须重启服务
答案:AB。Vite 对预构建的依赖产物使用 Cache-Control: max-age=31536000, immutable 强缓存,因为依赖文件带内容 hash、几乎不变;对源码文件使用协商缓存(ETag/304),因为源码经常变动,需要服务器判断是否重编译。C 错,源码用强缓存会导致改动不生效;D 错,缓存失效只需要重新请求,不需要重启服务。
Q7. 关于 Vite 与 Webpack 差异正确的是?
- A.Vite 开发期按需加载启动快← 正确答案
- B.Vite 生产构建默认用 Rollup← 正确答案
- C.Webpack 热更新粒度总是更细
- D.Webpack 开发期启动需全量打包← 正确答案
答案:ABD。Vite 开发期按需加载,冷启动快;生产构建默认交给 Rollup,与开发模式是两套引擎。Webpack 开发期启动确实需要先全量打包。C 错在“总是”——Vite 的 HMR 粒度是单个模块,通常比 Webpack 的 chunk 级更细。即时厨房单菜快不等于宴会流程就好,两者各有适用场景。
Q8. 浏览器加载 ESM 时 import 语句对应什么?
- A.一个模块文件网络请求← 正确答案
- B.一次服务器磁盘预编译
- C.一个页面级的整体刷新
- D.一次文件压缩解压操作
答案:A。浏览器解析到 import 语句后,会按模块路径发起 HTTP GET 请求获取对应文件,然后按模块执行。这不是磁盘操作、不是页面刷新、也不是压缩操作。import 就是阿伟按下传菜铃,浏览器顺着铃响去取那道菜。
Q9. Vite 生产构建选 Rollup 而非 esbuild 的深层原因?
- A.esbuild 无法处理依赖预构建
- B.esbuild 打包产物无法做代码分割
- C.Rollup 的插件生态更完整且产物优化更成熟← 正确答案
- D.Rollup 的算法效率远高于 esbuild
答案:C。esbuild 确实能做依赖预构建,也支持有限代码分割,但它更侧重于“快”;生产构建需要成熟的 tree-shaking、代码分割策略、插件生态和产物优化能力,Rollup 在这方面积累更深厚。D 说反了,esbuild 的 Go 实现比 Rollup 更快。就像即时厨房出小炒快,但办大型宴会的全套流程,还是老牌宴会团队更稳。
Q10. 原生 ES 模块(ESM)的特点有哪些?
- A.import 是静态声明可静态分析← 正确答案
- B.浏览器按 URL 并发加载模块← 正确答案
- C.模块默认共享全局作用域
- D.必须在打包后才能在浏览器运行
答案:AB。ESM 的 import 是静态声明,构建工具可以在不执行代码的情况下分析依赖图;浏览器会为每个模块 URL 发起并发请求。C 错,ESM 每个模块有独立作用域;D 错,现代浏览器原生支持 ESM,不需要打包也能运行——这正是 Vite 开发模式“不打包”的技术前提。