Webpack Fast Refresh 与 Vite
Source: Dev.to
请提供您希望翻译的具体文本内容,我将按照要求保留源链接并将文本翻译为简体中文。
Source: …
概览
本文分享了在 ilert‑ui——一个拥有众多懒加载路由的大型 React + TypeScript 应用——的日常开发中,哪些做法感觉最快。我们首先从 Create React App (CRA) 转向现代化工具,尝试了 Vite 用于本地开发,最终落地到 webpack‑dev‑server + React Fast Refresh。
本文最初发布在 ilert 博客上,完整版本可在此处查看。
范围:仅限本地开发。我们的生产构建仍然使用 Webpack。供参考,React 官方团队已于 2025 年 2 月 14 日 正式停用 CRA,建议迁移到框架或诸如 Vite、Parcel、RSBuild 等现代构建工具。
来自 ilert‑ui 的定性现场笔记:我们没有进行正式的基准测试;这只是我们在大型路由拆分应用中的日常体验。
常用术语
| 术语 | 定义 |
|---|---|
| HMR | 在不进行完整刷新(full reload)的情况下,将修改后的代码注入正在运行的应用。 |
| Lazy route / code‑splitting | 仅在访问对应路由时才加载该路由的代码。 |
| Vendor chunk | 跨路由共享的第三方依赖打包块,可被缓存。 |
| Eager pre‑bundling | 预先打包公共依赖,以避免后续产生大量小请求。 |
| Dependency optimizer (Vite) | 预打包裸导入;如果在运行时发现新依赖,可能会重新执行。 |
| Type‑aware ESLint | 使用 TypeScript 类型信息的 ESLint——更精准但更重。 |
为什么 CRA 不再适合 ilert‑ui
ilert‑ui 随着应用的成熟已经超出了 CRA 的便利默认设置。我们放弃 CRA 的主要原因如下:
- 自定义摩擦 – 高级的 webpack 调整(自定义 loader、更严格的 split‑chunks 策略、
react-refresh的 Babel 设置)需要弹出(eject)或打补丁,这减慢了在生产规模应用上的迭代速度。 - 庞大的依赖面 –
react-scripts引入了大量的传递依赖。安装时间增长,安全噪声也随之增加,却没有为我们带来明显的好处。
下一步目标
- 保持 React + TS。
- 改善服务器启动后的 time‑to‑interactive。
- 在编辑时保留状态(Fast Refresh 行为),并保持 HMR 的流畅性。
- 在跨多个懒加载路由导航时,保持可预测的首次访问延迟。
Vite:第一印象
在开发过程中,Vite 将你的源代码作为原生 ESM 提供,并使用 esbuild 对 node_modules 中的裸导入进行预打包。这通常能带来非常快的冷启动和响应式 HMR。
我们立刻喜欢的点
- Cold starts – 明显比我们的 CRA 基准更快。
- Minimal config, clean DX – 合理的默认设置和易读的错误信息。
- Great HMR in touched areas – 在已访问的路由中编辑时体验极佳。
规模带来的挑战
- Methodology – 来自 ilert‑ui 日常开发的定性观察。
- Repo shape – 数十个懒加载路由,若干大型模块拉入大量依赖;数百个共享文件以及跨特性的深层 store 导入。
我们注意到的
- First‑time heavy routes – 打开依赖丰富的路由时常会触发大量 ESM 请求,有时还会重新运行依赖优化器。跨路由探索未触及的路由时,感觉比我们使用 webpack(会主动预打包共享 vendor)的设置更慢。
- Typed ESLint overhead – 在开发服务器进程中运行支持类型的 ESLint(使用
parserOptions.project或projectService)会在编辑时增加延迟。将 linting 移到进程外有帮助,但在我们的规模下仍未完全抵消成本——这也是带类型 lint 的预期权衡。
TL;DR(针对我们的代码库):一旦在会话中触及某个路由,Vite 表现非常出色,但在众多懒加载路由的首次访问时表现不够可预期。
我们在生产环境中运行的内容
// webpack.config.js
module.exports = {
optimization: {
minimize: false,
runtimeChunk: "single",
splitChunks: {
chunks: "all",
cacheGroups: {
"react-vendor": {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
name: "react-vendor",
chunks: "all",
priority: 30,
},
"mui-vendor": {
test: /[\\/]node_modules[\\/](@mui\\/material|@mui\\/icons-material|@mui\\/lab|@mui\\/x-date-pickers)[\\/]/,
name: "mui-vendor",
chunks: "all",
priority: 25,
},
"mobx-vendor": {
test: /[\\/]node_modules[\\/](mobx|mobx-react|mobx-utils)[\\/]/,
name: "mobx-vendor",
chunks: "all",
priority: 24,
},
"utils-vendor": {
test: /[\\/]node_modules[\\/](axios|moment|lodash\\.debounce|lodash\\.isequal)[\\/]/,
name: "utils-vendor",
chunks: "all",
priority: 23,
},
"ui-vendor": {
test: /[\\/]node_modules[\\/](@loadable\\/component|react-transition-group|react-window)[\\/]/,
name: "ui-vendor",
chunks: "all",
priority: 22,
},
"charts-vendor": {
test: /[\\/]node_modules[\\/](recharts|reactflow)[\\/]/,
name: "charts-vendor",
chunks: "all",
priority: 21,
},
"editor-vendor": {
test: /[\\/]node_modules[\\/](@monaco-editor\\/react|monaco-editor)[\\/]/,
name: "editor-vendor",
chunks: "all",
priority: 20,
},
"calendar-vendor": {
test: /[\\/]node_modules[\\/](some-calendar-lib)[\\/]/,
name: "calendar-vendor",
chunks: "all",
priority: 19,
},
// …additional vendor groups as needed
},
},
},
// other webpack config (loaders, plugins, devServer, etc.)
};
注意: 上面的代码片段展示了相关的
splitChunks配置。其余的 webpack 配置(加载器、插件、devServer 等)保持不变。
为什么这种设置让我们的团队感受到端到端更快
- 主动的 vendor 预打包 – 我们显式地预先打包 vendor chunk(React、MUI、MobX、图表、编辑器、日历等)。首次加载会稍微重一点,但访问其他路由时因为共享依赖已被缓存,加载速度更快。
SplitChunks让这种行为可预测。 - React Fast Refresh 的使用体验 – 编辑时状态保持稳固,错误恢复可靠,覆盖层表现符合我们的期望。
- 非阻塞的 lint 检查 – Typed ESLint 在 dev server 进程之外运行,即使进行大型类型检查,HMR 仍保持响应。
要点
- Vite 在已编辑路由上进行快速迭代时表现出色,这得益于其原生 ESM 服务和闪电般的 HMR。
- 使用 eager vendor splitting 的 Webpack 在大型代码库中对许多懒加载路由的首次导航提供更可预测的延迟。
- 将繁重的工具(具备类型感知的 ESLint、类型检查)与开发服务器分离 能在不论选择哪种打包器的情况下,保持流畅的开发者体验。
如果你正在评估一个类似规模的 React + TS 应用的迁移路径,请考虑上述权衡,并选择最符合团队工作流和性能优先级的工具链。
// webpack.config.js (excerpt)
{
// …
splitChunks: {
cacheGroups: {
"calendar-vendor": {
test: /[\/\\]node_modules[\/\\](@fullcalendar\/core|@fullcalendar\/react|@fullcalendar\/daygrid)[\/\\]/,
name: "calendar-vendor",
chunks: "all",
priority: 19,
},
vendor: {
test: /[\/\\]node_modules[\/\\]/,
name: "vendor",
chunks: "all",
priority: 10,
},
},
},
// …
}
// vite.config.ts – Vite `optimizeDeps` includes we tried
export default defineConfig({
optimizeDeps: {
include: [
"react",
"react-dom",
"react-router-dom",
"@mui/material",
"@mui/icons-material",
"@mui/lab",
"@mui/x-date-pickers",
"mobx",
"mobx-react",
"mobx-utils",
"axios",
"moment",
"lodash.debounce",
"lodash.isequal",
"@loadable/component",
"react-transition-group",
"react-window",
"recharts",
"reactflow",
"@monaco-editor/react",
"monaco-editor",
"@fullcalendar/core",
"@fullcalendar/react",
"@fullcalendar/daygrid",
],
// Force pre‑bundling of these dependencies
force: true,
},
});
Vite – 优势
- 冷启动速度极快,配置轻量。
- 在已访问的路由中实现出色的 HMR。
- 插件生态系统强大,默认使用现代 ESM。
Vite – Cons
- 依赖优化器的重新运行可能会在首次导航跨多个懒加载路由时中断流程。
- 在大型 monorepo 和使用链接包时需要仔细设置。
- 在进程内运行的 Typed ESLint 可能会影响大型项目的响应性;最好在进程外运行。
Webpack + Fast Refresh – 优势
- 通过预加载供应商块,实现跨多个路由的可预测首次访问延迟。
- 对加载器、插件和输出进行细粒度控制。
- Fast Refresh 能保留状态,并拥有成熟的错误覆盖层。
Webpack + Fast Refresh – 缺点
- 初始加载比 Vite 的冷启动更重。
- 需要维护的配置面更大。
- 历史遗留的复杂性(通过现代配置模式和缓存得到缓解)。
选择 Vite 的情形:
- 冷启动在你的工作流中占主导。
- 你的模块图并不庞大或没有被拆分成大量懒加载路由。
- 插件——尤其是带类型的 ESLint——轻量或在进程外运行。
选择 Webpack + Fast Refresh 的情形:
- 你的应用受益于提前预打包的供应商代码以及在多个路由上的可预测的首次访问延迟。
- 你想要对 loader / plugin 和构建产出进行精确控制。
- 你喜欢 Fast Refresh 的状态保留和覆盖层。
在 iLert 博客了解更多。