最大的 React 性能杀手不就是你?
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 35/100
- issue の種類
- ドキュメント
- 明瞭さ
- 説明が足りない
- 活発さ
- 静か
- 技術スタック
- javascript, react
- 領域
- content
調査の方向性
The issue body contains a complete Chinese article about React.memo, useCallback, useMemo, and React DevTools, but names no repository file, test, or entry point. First confirm the intended publication location and required editorial change; done should mean the article's scope, placement, and acceptance criteria are explicitly agreed.
索引モデルが issue の本文から書いたものです。
説明
你好,我是冴羽。
你写的 React.memo 可能根本没用!
你可能觉得自己已经做了性能优化——给组件包了 React.memo,给回调加了 useCallback,给计算值用了 useMemo。
但如果你在传递 props 时写成这样:
<UserCard
style={{ padding: 16, borderRadius: 8 }}
onSelect={() => handleSelect(user.id)}
config={{ showAvatar: true, compact: false }}
user={user}
/>
恭喜你,你的优化白做了~
1. 为什么 React.memo 会失效?
为什么呢?
因为 React 比较的是引用,不是内容。
当你写 React.memo 包裹一个组件时,React 会在父组件重新渲染时比较新旧 props。
如果所有 props 都“相等”, React 就跳过子组件的渲染,直接复用上次的结果。
问题来了——React 用什么判断“相等”?
答案是 Object.is,也就是引用相等:
Object.is({ padding: 16 }, { padding: 16 }) // false
Object.is(() => {}, () => {}) // false
即使内容完全一样,引用不同就是不同。 React 就会认为 props 变了,然后重新渲染子组件。
这就是为什么内联对象和回调函数是隐藏的性能杀手——它们每次渲染都会创建新引用,让 React.memo 形同虚设。
以前你以为 React.memo 在帮你省性能,实际上,每次都在重新渲染。
2. 什么时候这个问题会要命?
我得先说清楚:不是所有内联 props 都是性能 bug。
如果子组件很轻量,渲染频率很低,也没用 memo,那内联写法完全没问题。
React 官方文档也一直强调:先测量,别瞎优化。
但当这三个条件同时出现时,你就要注意了:
-
父组件频繁重新渲染(比如搜索输入、滚动状态、筛选器)
-
子组件或子树足够大,重新渲染的成本很高
-
你已经加了 memo,期待 React 跳过不必要的渲染
在这种场景下,不稳定的内联引用不是“增加一点开销”——它会直接废掉了你精心设计的优化!
更要命的是,这个问题不会报错,不会警告,UI 照常工作。
它只会悄悄地让你的列表过滤器变卡、输入延迟、火焰图爆炸。
3. 我做了个实验:200 行列表的性能崩溃
为了证明这个问题有多严重,我搭了个测试场景:
-
一个可搜索的商品列表
-
200 个用
React.memo包裹的ProductRow组件 -
每个组件接收相同的逻辑值,但每次父组件渲染都传入新的对象和函数引用
代码长这样:
{filteredProducts.map(p => (
<ProductRow
key={p.id}
product={p}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 20px',
borderBottom: '1px solid #eee'
}}
onAddToCart={(id) => console.log('Added:', id)}
/>
))}
结果惨不忍睹:
-
输入 6 个字符后,每个可见行显示
Renders: 14 -
React DevTools Profiler 显示:单次按键触发的渲染耗时 243.9ms
-
火焰图里,所有 200 个子组件全部点亮
也就是说,React.memo 完全失效了。
因为 style 对象和 onAddToCart 回调每次都是新创建的,memo 的 props 比较每次都是失败的。
我还用了 why-did-you-render 这个工具来诊断。它直接告诉我:
-
props.style是“内容相同但对象不同” -
props.onAddToCart是“同名但函数不同”
这就是引用不匹配的铁证。
4. 怎么改?很简单
要让 React 的 bailout 机制生效,你需要稳定的引用。
改法 1:把静态对象移到模块作用域
// ✅ 在组件外部定义,只创建一次
const ROW_STYLE = {
display: 'flex',
justifyContent: 'space-between',
padding: '12px 20px',
borderBottom: '1px solid #eee'
};
export default function ProductList() {
// ...
return (
<ProductRow
style={ROW_STYLE}
// ...
/>
);
}
改法 2:用 useCallback 包裹动态回调
export default function ProductList() {
const [searchTerm, setSearchTerm] = useState('');
// ✅ 依赖数组为空,函数引用保持稳定
const handleAddToCart = useCallback((id) => {
console.log('Added:', id);
}, []);
return (
<ProductRow
onAddToCart={handleAddToCart}
// ...
/>
);
}
修复后的效果:
-
ProductList渲染时间从 243.9ms 降到 6ms -
无论怎么输入,渲染计数始终停在
2 -
why-did-you-render不再报警
性能直接提升 40 倍!
5.所以什么时候该优化,什么时候别管?
如果子组件依赖引用相等来跳过渲染,那父组件就必须传稳定的引用。如果子组件根本没 memo,或者渲染成本很低,那你稳定引用也没意义。
我的建议是:
-
静态值优先外提: 如果一个对象永远不变,把它移到组件外部,零成本解决问题
-
动态值按需 memo: 只在子组件真的能从稳定引用中受益时,才用
useCallback和useMemo -
先 Profile 再优化: 别瞎猜,用 React DevTools Profiler 测一下再说
React 官方文档也是这么说的:能外提就外提,需要缓存再用 Hooks。
关于 React Compiler:
你可能听说过 React Compiler 会自动帮你做这些优化。确实,它能在编译时自动 memoize 很多代码,减少手动写 useMemo 和 useCallback 的需求。
但这不意味着引用稳定性就不重要了。React Compiler 的文档也说了:useMemo 和 useCallback 在某些场景下仍然有用,比如你需要精确控制 Effect 的依赖。
所以即使用了 Compiler,理解引用不稳定如何影响重新渲染,依然是必修课。
6. 最后一句话
内联对象和内联回调不是“错误代码”。大部分时候,它们就是普通的 JSX 表达式。
但当它们穿过 memo 边界时,游戏规则就变了。
这个问题值得更多关注,因为它太容易在生产环境里悄悄发生——代码看起来很干净,应用运行正常,但你以为买到的性能优化其实根本没生效。
所以给想写快速 React 应用的团队一个建议:
先 Profile。如果 memo 的子树还在频繁渲染,先检查 props,别急着怪 React。把静态对象移出渲染路径。只在子组件真正受益时才 memoize 回调。用 React DevTools 和 Why Did You Render 确认到底什么变了、为什么变。
坚持这么做,React.memo** 就不再是装饰性的性能代码,而是真正在干活。**
我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。
欢迎围观我的“网页版朋友圈“,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。
- 主要言語
- 言語のデータがありません
- スター
- 31.1k
- フォーク
- 4.6k
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
mqyqingfeng/Blog のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
mqyqingfeng/Blog#437 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 42/100
mqyqingfeng/Blog#448 ·
-
難易度 1/5 1時間未満 初心者へのやさしさ 15/100
mqyqingfeng/Blog#447 ·
-
我一直想搞套自动化创作工作流,结果……オープン
難易度 3/5 1〜2日 初心者へのやさしさ 25/100
mqyqingfeng/Blog#446 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 45/100
mqyqingfeng/Blog#445 ·
mqyqingfeng/Blog の issue をすべて見る
似ている issue
-
correction metadata
難易度 2/5 1時間未満 初心者へのやさしさ 72/100
acl-org/acl-anthology#10431 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 75/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
IQSS/dataverse.org#83 ·
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
難易度 1/5 1時間未満 初心者へのやさしさ 78/100
lingdojo/kana-dojo#32435 · コメント 1 件 · リアクション 5 件 ·
メンテナーはふだん 1 日以内に返信
-
data hacktoberfest help wanted
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
pacocartones/free-llm-api-hub#307 ·
メンテナーはふだん 1 日以内に返信