前端性能优化基础
列表 key、useMemo/useCallback、路由懒加载、资源优化
- 理解列表 key 为什么影响性能
- 知道 useMemo/useCallback 是干嘛的、别滥用
- 了解路由级代码分割与懒加载
- 知道图片字体优化的基本手段
- 对 Core Web Vitals 建立印象
性能优化像收拾书包:出门只带当下要用的东西,别把一学期的书全背上。首屏别一股脑加载所有页面的代码和图片,用到哪个再加载哪个;昂贵的计算算过一次就记下来,别每次渲染都重算。
为什么会变慢?
React 默认状态一变就重渲染相关组件。如果列表很长、每次渲染都重新算一大堆、或者首屏一下下载了整个网站的代码,界面就会卡。优化的核心就两件事:少渲染、少下载。
列表 key 与减少重复渲染
没有正确 key,React 更新列表时只能猜哪些项变了,常常整列重渲染;有了稳定 key,它只动真正变化的那一行。
useMemo 与 useCallback:缓存,但别滥用
useMemo 缓存一次昂贵计算的结果,依赖不变就复用;useCallback 缓存函数引用,避免子组件因拿到「新函数」而无谓重渲染。注意:它们本身有成本,只对真正昂贵的计算或被 memo 的子组件用,别到处包。
import { useMemo, useCallback } from "react";
function OrderList({ items }) {
// 只有 items 变了才重新算总价
const total = useMemo(
() => items.reduce((s, it) => s + it.price, 0),
[items]
);
// 缓存回调引用,传给被 React.memo 包裹的子组件
const onRemove = useCallback((id) => {
setList(items.filter(it => it.id !== id));
}, [items]);
return <div>总价:{total} <Item onRemove={onRemove} /></div>;
}useMemo 不是免费的:它本身要存储上次的值、每次渲染都要比较依赖数组有没有变。如果计算本身很便宜(比如两个数相加),那「存和比」的开销可能比「当场算一遍」还大,纯属负优化。经验法则:先别加,等 Profiler 证明某个计算真的昂贵、或真有子组件在无谓重渲染时,再精准地给那一处加上。到处包 useMemo/useCallback 只会让代码更难读。
动手:用 Python 数重渲染次数与 memo
下面演示「没有缓存时昂贵函数被反复调用」和「加了缓存后只算一次」的差别,直观感受 memo 的价值。
怎么定位慢:React DevTools Profiler
别凭感觉猜哪里慢。React DevTools 有个 Profiler 面板:点录制,操作一遍界面(比如在输入框打字),它会画出每次提交时哪些组件重渲染了、各花了多少毫秒。颜色越红说明越久,点组件能看到它这次是因为什么 props/state 变化而重渲染。你会很容易发现「我只在输入框打字,结果整个 1000 行列表全红了」——那就是要上 key、memo、或者把列表拆成虚拟滚动的信号。没有 Profiler 数据就盲目优化,方向大概率是错的。
路由懒加载与资源优化
路由级代码分割:把每个页面拆成单独的包,首屏只加载当前页,点到别的路由才加载对应代码(React.lazy / Next.js 自动做)。图片用现代格式、按需加载(loading="lazy")、指定宽高避免布局跳动;字体做子集化或 font-display: swap。
还有个长列表神器叫虚拟滚动:哪怕你有一万条数据,屏幕一屏也只能显示十几条。与其把一万个 DOM 节点都渲染出来(浏览器会卡爆),不如只渲染当前可视区域那十几条,滚动时动态替换内容。配合 key 和 memo,长列表卡顿基本就解决了。库如 react-window 已经封装好,知道思路即可。
性能优化的第一原则是先测量、再动手。大多数界面慢,瓶颈就一两处,用 Profiler 定位到那一两处精准修,比全局包 memo、拆组件有效得多。 Premature optimization 会让代码变复杂、还可能优化错地方。先写清晰正确的代码,等真卡了再用工具找原因。
把这一课压缩成行动清单:先给列表稳定 key,再用 Profiler 找到真正重渲染的热点,只对那几处上 memo;首屏只加载当前路由的代码和图;长列表上虚拟滚动。按这个顺序做,性价比最高。
Google 用三个指标衡量真实用户体验,深挖在 s13-m5-l4。先记住直觉:LCP(最大内容绘制)= 用户看到主内容加载完花了多久,越短越好,主要卡首屏代码和大图;INP(交互到下一次绘制)= 用户点按钮后界面多久能响应,主要卡长任务和不必要的重渲染;CLS(累积布局偏移)= 页面内容有没有「突然跳动」,比如图片没指定宽高加载完把文字挤下去,指定尺寸就能避免。入门先记住:首屏只发必要的代码和图,贵计算缓存住。
渲染列表时,给每一项设置正确 key 的主要作用是?
资深工程师加餐
底层原理 · 大厂视角 · 工程经验,点卡片展开
穿透是反复查「根本不存在」的数据,用布隆过滤器或缓存空值挡;击穿是某个热点 key 过期瞬间大量请求压向数据库,用互斥重建或逻辑过期;雪崩是大批 key 同时过期,给 TTL 加随机抖动。缓存与库的一致性常用「先更新数据库、再删除缓存」,并靠重试/订阅补偿。
挑战任务
手写一个 memo 缓存
memo 的本质:同样的入参,直接返回上次的结果,不再重算。实现 memo_add(a, b):用字典缓存,键是 (a, b)。
课后作业
判断一个计算值不值得用 useMemo
useMemo 本身有存储和比较开销,只有真正昂贵的计算才值得缓存。实现 worth_memo(cost_ms):耗时超过 5.0 毫秒才返回 True(值得缓存),否则 False(当场算更便宜)。打印五个耗时样本的判定。