React 以性能优势和声明式 UI 模型闻名,但很多开发者依然会疑惑:
"为什么我的 React 应用用起来有点卡,即使我已经遵循了各种最佳实践?"
问题的根源往往在于:未优化的渲染 和 组件生命周期 / memo 化的隐形陷阱。 本文将带你深入底层,理解为什么会发生不必要的渲染、React 的 Diff 算法(Reconciliation)是如何工作的,并给出具体的优化策略。
🎯 "真正快" ≠ "感觉快"
有一个重要的 UX 概念叫 感知性能 : 即使某个渲染本身很快,如果它触发了 成千上万次微小的 DOM 或 React 树更新,用户依然会感受到卡顿、抖动,甚至页面"掉帧"。
🧪 一个看似无害的组件
jsx
// components/CommentList.js
import React from 'react';
// Comment 组件:接收单条评论数据并渲染
const Comment = ({ comment }) => {
console.log("Rendering comment", comment.id); // 打印渲染日志,方便观察渲染次数
return <div>{comment.text}</div>; // 显示评论内容
};
// CommentList 组件:接收评论列表并逐条渲染 Comment
const CommentList = ({ comments }) => {
return (
<div>
{comments.map(comment => ( // 遍历评论数组
<Comment key={comment.id} comment={comment} /> // 为每条评论生成子组件
))}
</div>
);
};
export default CommentList; // 导出 CommentList 组件
看起来没问题吧?
试试在父组件里这样调用:
jsx
<CommentList comments={comments} /> // 传入评论列表
然后在父组件里随便调用 setState
,你会发现: 所有评论组件都会重新渲染 ------ 即使它们的内容完全没变。
这就是所谓的 "千刀万剐式卡顿"。
👀 为什么会这样?
因为:父组件更新 → 子组件默认跟着更新 。 React 只会对 memo 化组件 做浅比较(shallow compare)。
如果 props 是新建的对象或数组,===
恒为 false,React 就认为需要更新。
jsx
<CommentList comments={[...comments]} />
// 每次渲染都会生成一个新的数组引用
💡 解法一:React.memo() 📦
用 React.memo
包裹函数组件:
jsx
// 用 React.memo 包裹 Comment 组件,避免不必要的重复渲染
const Comment = React.memo(({ comment }) => {
console.log("Rendering comment", comment.id); // 打印渲染日志
return <div>{comment.text}</div>; // 显示评论文本
});
优点: 跳过无意义的重新渲染 缺点: 滥用可能反而拖慢性能(额外的 props 对比开销)
👉 前提是:props 引用必须保持稳定。
🛑 红色警告:每次都生成新 props
jsx
<CommentList comments={[...comments]} /> // 每次都创建一个新数组
即使内容没变,React 也会认为 comments
是新值,导致所有子组件都重渲染。
✅ 正确做法:用 useMemo
或 state 保持稳定引用:
jsx
// 用 useMemo 保证 comments 引用在内容没变时保持稳定
const stableComments = useMemo(() => comments, [comments]);
<CommentList comments={stableComments} /> // 传入稳定引用
💥 真正的性能杀手:useEffect + 额外渲染
常见问题:useEffect
的依赖总是新对象。
jsx
useEffect(() => {
fetchData(); // 每次渲染都重新调用
}, [filter]); // filter 总是新对象
如果这样写:
jsx
fetchData({ name: 'John' }); // 每次都是新对象 { name: 'John' }
即使 name
没变,useEffect
依然会执行。
📌 正确做法:只在值真正变化时才生成新对象:
jsx
// 用 useMemo 保持对象引用稳定,只有 name 变化时才生成新对象
const stableFilter = useMemo(() => ({ name }), [name]);
useEffect(() => {
fetchData(stableFilter); // 依赖稳定对象
}, [stableFilter]);
🔍 深入理解:React 的 Reconciliation
React 的 Reconciliation(协调)算法通过浅比较来判断节点是否需要更新:
- ✔️ 相同引用 → 不更新
- ❌ 新对象/数组 → 标记为更新
所以如果你每次都新建对象/数组,React 就会认为"全部需要更新"。
🧠 高阶技巧:useCallback 与 useMemo
函数同样存在引用不稳定的问题。
jsx
const handleClick = () => doSomething(id); // 每次渲染都会生成新函数
如果子组件接收了这个函数作为 props,即使逻辑不变,也会触发子组件重渲染。
✅ 正确做法:
jsx
// 用 useCallback 保持函数引用稳定
const handleClick = useCallback(() => doSomething(id), [id]);
<Button onClick={handleClick} /> // 子组件不会因为函数引用变化而重渲染
🚀 实战案例: 在我们公司一个老旧仪表盘项目中,引入 useMemo
和 useCallback
后,渲染次数减少了 76%,逻辑完全没改,只是稳定了引用。
🎁 Bonus 工具:why-did-you-render
调试利器:能帮你发现 不必要的渲染。
bash
npm install @welldone-software/why-did-you-render
配置方式:
js
import React from 'react';
import whyDidYouRender from '@welldone-software/why-did-you-render';
// 启用渲染监控,自动追踪所有纯组件
whyDidYouRender(React, {
trackAllPureComponents: true,
});
现在控制台会告诉你:每次渲染为什么发生。
🧹 总结:React 渲染优化清单
- 给纯组件加
React.memo()
- 用
useMemo
保持对象/数组引用稳定 - 用
useCallback
保持函数引用稳定 - 避免在 JSX 里写匿名函数
- 使用调试工具定位不必要渲染
✨ 最后感悟
你的 React 应用可能并不是真的慢,而是 多余的重新渲染 让它"看起来"很慢。
优化渲染流的关键,不是写多少代码,而是 保持引用稳定。
花点时间追踪渲染、稳定 props,你会发现应用顺滑得多。你的用户 ------ 以及未来的你 ------ 都会感谢现在的优化。
要不要我帮你在文章里加一个 "优化前后渲染次数对比表格/图",让效果更直观?