Vite 为什么快?开发环境和生产环境分别怎么构建?

进阶高频原理对比约 6 分钟读完

一句话回答

Vite 开发时不打包:dev server 直接以原生 ES Module 的形式提供源码,浏览器请求到哪个模块才按需编译哪个,启动时间基本不随项目规模增长。第三方依赖在启动时做一次依赖预构建(CommonJS 转 ESM、把内部文件很多的包合并成一个模块),并配合强缓存。生产环境仍然打包,因为未打包的 ESM 嵌套 import 会带来大量请求和网络往返。早期 Vite 开发时用 esbuild 预构建、生产用 Rollup 打包;Vite 8 起换成 Rust 编写的 Rolldown(预构建、打包)和 Oxc(转译、压缩),开发和生产用同一套底层工具。

详细解析

和 webpack dev server 的差别

文本
webpack dev server:
  入口 → 构建整个模块图 → 打包 → 启动服务(项目越大越慢)

Vite dev server:
  启动服务(几乎立即)→ 浏览器请求 /src/main.ts → 编译这一个文件返回
                       → 浏览器解析其中的 import,再请求下一个模块

webpack 必须先把所有模块构建完才能提供服务,改一个文件后还要重新生成受影响的 chunk。Vite 把"找依赖、加载模块"这件事交给浏览器:页面里 script type="module" 引入入口,浏览器遇到 import 就发请求,Vite 收到请求再编译对应文件(如 TS 转 JS、Vue 单文件组件转 JS)。没有访问到的路由页面,根本不会被编译。

依赖预构建

源码按需编译,第三方依赖则在首次启动时预构建,结果放在 node_modules/.vite:

  1. 兼容 CommonJS 和 UMD:浏览器只认 ESM,很多包仍然只发布 CommonJS,需要先转成 ESM,并让 import { x } from 'cjs-pkg' 这类具名导入能用
  2. 减少请求数:有些 ESM 包内部有几百个文件(官方文档举的例子是 lodash-es),不合并的话一个 import 就会触发几百个请求
  3. 重写裸模块路径:源码里的 import vue from 'vue' 会被改写成 /node_modules/.vite/deps/vue.js?v=xxx 这样浏览器能请求的地址

预构建只在开发环境做。lock 文件、vite.config 中相关配置、NODE_ENV 等发生变化时会重新预构建;也可以用 vite --force 或删除 node_modules/.vite 强制重来。

HTTP 缓存

  • 预构建后的依赖带上版本查询参数,响应头是 Cache-Control: max-age=31536000,immutable,缓存后不再请求 dev server
  • 源码模块用协商缓存(304 Not Modified),文件没改就不重新传输

所以刷新页面时,大部分请求都不会真正到达 dev server。

生产环境为什么还要打包

开发环境在本机,请求延迟几乎为零;生产环境中,未打包的 ESM 会形成"请求入口 → 解析出依赖 → 再请求依赖"的瀑布,即使有 HTTP/2 也会多出很多网络往返。打包还能做 tree shaking、代码分割、压缩、按内容生成哈希文件名,这些都是生产环境需要的。

早期 Vite 开发用 esbuild(预构建、转译 TS),生产用 Rollup 打包,两套工具的转换行为和插件体系不同,偶尔会出现"开发正常、打包后出问题"。Vite 8 把依赖预构建和生产打包都换成了 Rolldown,JS 转译和压缩换成了 Oxc,esbuild 变成可选依赖,就是为了减少两套工具带来的差异。配置上 build.rollupOptions 改名为 build.rolldownOptions(旧名已弃用,暂时还能用),esbuild 配置项也已弃用、会自动转换成 oxc,升级时以官方迁移指南为准。

开发和生产不一致的风险

场景 开发环境 生产环境
模块加载 浏览器原生 ESM,逐个请求 打包后的 chunk
CommonJS 依赖 预构建时转换 打包时转换,处理方式可能不同
CSS 顺序 按模块执行顺序插入 style 提取成独立文件,顺序可能变化
环境变量 import.meta.env.DEV 为 true 被静态替换,死代码会被删掉

上线前一定要用 vite build 加 vite preview 在本地验证一遍生产产物。

面试官可能追问

项目很大时,Vite 开发环境会不会也变慢?

会在另一个地方变慢:首次打开页面时,浏览器要发出成百上千个模块请求,请求数成了瓶颈,尤其是大量小文件的组件库没有被预构建时。缓解办法是控制模块数量、避免从大量文件的入口文件(barrel 文件)整体导入,必要时把依赖加进 optimizeDeps.include。Vite 官方也在探索开发时打包的模式来解决这个问题。

Vite 的插件和 Rollup 插件是什么关系?

Vite 的插件接口扩展自 Rollup 的插件接口,resolveId、load、transform 等钩子在开发和生产都会调用,所以很多 Rollup 插件能直接用。Vite 另外加了 config、configureServer、transformIndexHtml 等专有钩子,只在 Vite 中生效。

开发时新加了一个依赖,Vite 会怎么处理?

Vite 启动时会扫描源码里的裸模块导入作为预构建的入口。运行中发现了没有预构建过的新依赖,会重新执行预构建,必要时刷新页面。如果依赖是动态导入、扫描不到的,可以手动写进 optimizeDeps.include,避免首次访问时触发重新预构建。

Vite 为什么启动快,但首屏有时不如 webpack 快?

Vite 把编译工作从启动阶段挪到了请求阶段:启动不打包,但页面首次加载时要按需编译模块,还要发出大量请求。webpack 启动慢,但打包好以后一次返回少量文件。热更新时 Vite 只需要重新编译改动的那个模块,见 热更新的原理。

易错点

  • Vite 不是"开发和生产都不打包",生产环境一定会打包
  • 依赖预构建只处理 node_modules 里的依赖,源码依然是按需编译的
  • 开发环境靠浏览器原生 ESM 运行,不代表产物只能在支持 ESM 的浏览器运行,需要兼容旧浏览器时可以用官方的 @vitejs/plugin-legacy
  • 不要把"开发环境能跑"当成验收标准,开发和生产走的是不同的流程

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。