工程化与性能优化
模块 12 · 工程化与性能优化
模块 12 · 工程化与性能优化
这块是「中高级」和「初级」的分水岭。会用脚手架不算会,能解释为什么快、为什么慢、怎么调才算。 每一节都有 🔬 动手项 —— 在
fe-bootcamp/react/这个 Vite 项目里就能做。
12.1 构建工具原理 ⭐⭐
Webpack 的核心概念(五个)
| 概念 | 作用 |
|---|---|
| entry | 从哪开始找依赖 |
| output | 打包结果放哪、叫什么([contenthash] 就在这配) |
| loader | 转换单个文件(babel-loader、css-loader)—— 从右往左 / 从下往上执行 |
| plugin | 在构建生命周期的钩子上做事(HtmlWebpackPlugin、DefinePlugin) |
| mode | development / production,决定默认开哪些优化 |
Webpack 构建流程:初始化参数 → 创建 Compiler → 从 entry 开始递归 → 用 loader 转换每个模块 → 记录依赖关系构建依赖图 → 组装 Chunk → 输出。
🤔 loader 和 plugin 的区别一句话:loader 是文件转换器,plugin 是生命周期钩子上的扩展。
Vite 为什么快 ⭐⭐
| 开发时 | 生产构建 | |
|---|---|---|
| Webpack | 先打包整个 bundle 再启服务 → 项目越大启动越慢 | webpack |
| Vite | 不打包,直接用浏览器原生 ESM,按需编译请求到的模块 | Rollup(Vite 5+ 可选 Rolldown) |
Vite 快的两个关键:
- 依赖预构建:
node_modules里的第三方库用 esbuild(Go 写的,比 JS 快 10-100 倍)预打包成 ESM,且强缓存 - 源码按需转换:你的业务代码只在浏览器请求到它时才转换(所以启动时间和项目大小基本无关)
🤔 追问:Vite 开发和生产用两套工具(esbuild + Rollup),会不会有行为不一致?(会,这是它被诟病的点,所以生产构建前一定要跑一次
build+preview验证) 🤔 为什么 esbuild 不直接用于生产构建?(代码分割、CSS 处理等高级能力不如 Rollup 成熟)
🔬 实验
cd fe-bootcamp/react
npm run build -- --mode production然后:
- 看
dist/assets/里文件名的 hash,改一行代码重新 build,观察哪些文件 hash 变了、哪些没变 - 打开
dist/index.html,看 Vite 生成的<script type="module" crossorigin>和modulepreload npm run dev后打开 Network,把 filter 设成 JS,看到的是一个个源文件而不是一个 bundle —— 这就是 no-bundle
12.2 Tree Shaking ⭐
前提:ESM 的静态结构(模块 09 讲过)。CommonJS 的 require 是运行时的,摇不掉。
为什么有时候摇不干净:
- 模块有副作用(顶层执行了代码、修改了全局、import 了 css)→ 打包器不敢删
→
package.json里声明"sideEffects": false或["*.css"] - 你用了
import * as _ from 'lodash'或import _ from 'lodash'(CJS 包) → 改用lodash-es,或import debounce from 'lodash/debounce' - 类的方法很难被摇掉(打包器难以证明没被用到)
🔬 实验
在 react/src/App.jsx 里 import { format } from 'date-fns' 只用一个函数,build 后看产物体积;
再改成 import * as dateFns from 'date-fns',对比体积差异。
12.3 代码分割与懒加载 ⭐
三个层次:
- 路由级:
const Home = lazy(() => import('./Home'))—— 收益最大,优先做 - 组件级:重型组件(图表、编辑器、地图)按需加载
- 依赖级:
vendorchunk 单独拆,利用长期缓存
配套的资源提示:
<link rel="preload" href="critical.js" as="script"> <!-- 当前页面马上要用 -->
<link rel="prefetch" href="next-page.js"> <!-- 未来可能用,空闲时下载 -->
<link rel="preconnect" href="https://api.example.com"> <!-- 提前建连接 -->
<link rel="dns-prefetch" href="https://cdn.example.com">🤔 preload 和 prefetch 的优先级差别?滥用 preload 会怎样?(抢占带宽,反而拖慢关键资源)
🔬 实验
React 项目的 Lab 09 里已经有 lazy 的例子。跑 npm run build,看它是不是被拆成了独立 chunk。
12.4 Babel 与 AST
Babel 三步:parse(源码 → AST)→ transform(遍历并修改 AST,插件在这里工作)→ generate(AST → 代码)。
关键概念:
- @babel/preset-env + browserslist —— 根据目标浏览器决定要转译哪些语法
- polyfill vs 语法转译 —— 箭头函数是语法(Babel 转),
Promise/Array.prototype.includes是API(需要 core-js polyfill) useBuiltIns: 'usage'—— 按实际用到的 API 自动引入 polyfill,比entry更省体积
🤔 为什么现在很多项目不再需要 Babel?(esbuild/SWC 更快;且现代浏览器基本都支持 ES2020+,只有需要兼容老浏览器时才要它) 🔬 去 astexplorer.net 粘一段代码,看它的 AST 长什么样 —— 理解「代码即数据」,ESLint / Prettier / 各种 codemod 都建立在这上面。
12.5 HMR(热更新)原理
- 构建工具启动一个 WebSocket 服务
- 文件变化 → 重新编译变化的模块
- 通过 WS 通知浏览器:「模块 X 更新了」
- 浏览器端的 HMR runtime 请求新模块,执行模块的
accept回调替换旧模块 - 如果没有模块声明能处理这次更新 → 冒泡到顶 → 整页刷新
React 的 Fast Refresh 在此基础上额外做到了:替换组件实现的同时保留组件的 state。
🤔 为什么改一个组件不丢 state,改一个非组件的导出就会整页刷新?(Fast Refresh 只能安全地处理「只导出 React 组件」的模块)
12.6 性能优化清单 ⭐⭐(面试可以照这个框架答)
面试问「你做过哪些性能优化」,按加载链路分层答,比罗列技巧强一百倍:
1. 网络层
- HTTP/2、CDN、开启 gzip/brotli 压缩
- 缓存策略:静态资源 hash + 长缓存,HTML no-cache(模块 10.1)
- 减少 DNS/TCP 开销:preconnect、域名收敛
2. 资源层
- 体积:Tree Shaking、代码分割、按需引入、
rollup-plugin-visualizer分析包体积 - 图片:WebP/AVIF、响应式图片
srcset、懒加载loading="lazy"、雪碧图(HTTP/2 后价值下降) - 字体:
font-display: swap|optional、子集化、preload
3. 渲染层(模块 06)
- 关键 CSS 内联,非关键 CSS 异步
- JS 用
defer/async - 避免布局抖动、用 transform/opacity 做动画
- 长列表虚拟化
- 骨架屏 / SSR / SSG 优化首屏感知
4. 运行时(模块 01 + React Lab 08/09)
- 长任务切片、Web Worker
- 防抖节流
- React:memo 三件套、并发特性
5. 度量
- Lighthouse / Performance 面板 / RUM 上报 Core Web Vitals
- 先测量再优化
🤔 面试加分:说出每个手段的代价。比如「SSR 提升首屏,但增加了服务器成本和复杂度,还要处理同构代码的水合问题」。
12.7 Monorepo 与包管理(了解即可)
- npm / yarn / pnpm 的区别:pnpm 用硬链接 + 符号链接的内容寻址存储,省磁盘、快、且天然避免幽灵依赖(你没在 package.json 里声明的包,pnpm 下 import 不到)
- lock 文件的意义:锁定整棵依赖树的确切版本,保证任何机器上装出来一样
- 语义化版本:
^1.2.3允许 1.x.x,~1.2.3允许 1.2.x,1.2.3精确 - Monorepo:pnpm workspace / Turborepo / Nx —— 多包共享依赖、统一构建缓存
🤔
dependenciesvsdevDependenciesvspeerDependencies分别什么时候用?
12.8 TypeScript 常考点(前端现在基本是必选项)
| 主题 | 要点 |
|---|---|
interface vs type | interface 可声明合并、只能描述对象;type 能写联合/交叉/条件/映射类型。组件 props 用哪个都行,公共库倾向 interface |
| 泛型 | <T>、约束 T extends X、默认值 T = Y |
| 工具类型 | Partial Required Pick Omit Record ReturnType Parameters Awaited NonNullable —— 能手写实现最好 |
| 类型收窄 | typeof / in / instanceof / 字面量判别 / 自定义类型守卫 x is Y |
any / unknown / never | any 关闭检查;unknown 是安全的 any(用前必须收窄);never 表示不可能 |
| 断言 | as 和 ! 是「相信我」,是逃生舱不是解决方案 |
| 协变逆变 | 函数参数是逆变的(strictFunctionTypes)—— 冷门但会问 |
| 条件类型 | T extends U ? A : B、infer 提取类型 |
✍️ 手写工具类型练习(面试常考):
type MyPartial<T> = { [K in keyof T]?: T[K] }
type MyPick<T, K extends keyof T> = { [P in K]: T[P] }
type MyOmit<T, K> = ??? // 提示:用 Exclude
type MyReturnType<T> = ??? // 提示:用 infer
type DeepPartial<T> = ??? // 递归🔬 去 typescript-exercises 或 type-challenges 做前 10 道 easy 题。 建议:等 React 主线项目做到 Level 6 之后,把整个 Focus 项目改写成 TS —— 这比单独刷类型体操有用得多。
结业检查
- 能解释 Vite 为什么开发时快,以及它和 Webpack 的取舍
- 能说清 loader 和 plugin 的区别,并举例
- 知道自己项目的包体积构成(跑一次 visualizer)
- 能按「网络 → 资源 → 渲染 → 运行时 → 度量」五层框架讲性能优化
- 能手写
Partial/Pick/Omit/ReturnType
反向链接(0)
还没有笔记链接到这里。在 Obsidian 里用 [[笔记名]] 引用它,这里就会出现,关系图谱上也会多出一条连线。