"为什么我的表格又卡了?"
去年做数据中台项目时,我遇到一个诡异现象:一个渲染500行数据的Ant Design表格,每次勾选某行checkbox,整个页面都会卡顿近1秒。更离谱的是,我用React.memo包裹了每一行组件,理论上不该重渲染——你遇到过这种"明明用了优化手段却依然性能崩盘"的情况吗?
重渲染的底层逻辑:你以为的"没变"可能已经变了
问题的根源在于React的引用相等性检查机制:React.memo和useMemo确实会比对前后props,但如果父组件传给子组件的props是新创建的对象,即使内容完全一样,引用不同也会触发子组件重渲染。
在我们的场景中,父组件是这样传递数据的:
// 错误写法:每次渲染都生成新对象
const rowSelection = {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys)
};
return (
<Table
rowSelection={rowSelection} // 每次都是新对象!
dataSource={data}
columns={columns}
/>
);
通过React DevTools的组件重渲染高亮功能,我震惊地发现:每次勾选checkbox时,整个Table组件连同所有行组件都在闪烁(表示重渲染)。原来rowSelection对象在每次父组件渲染时都会被重新创建!
性能对比:一个useMemo能带来多大提升?
用useMemo包裹rowSelection后,性能数据差异惊人:
| 操作 | 优化前耗时 | 优化后耗时 |
|---|---|---|
| 勾选单行checkbox | 800ms | 30ms |
| 切换页码 | 1200ms | 200ms |
关键修改点:
// 正确写法:用useMemo保持引用稳定
const rowSelection = useMemo(() => ({
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys)
}), [selectedRowKeys]); // 依赖项要精确
避坑清单:这些重渲染陷阱你踩过几个?
- 内联对象/函数:
<Child style={{ color: 'red' }} />这种写法每次都会创建新对象 - 错误的使用Context:如果Context Provider的value没有用useMemo包裹,任何context变化会导致所有消费者重渲染
- 数组索引作为key:当列表顺序变化时,会导致React误判元素关系,触发不必要的DOM操作
- 过度使用状态提升:把本应属于子组件的状态提升到父组件,会导致父组件频繁重渲染
- 忽略React.StrictMode:开发环境下它会故意双渲染组件,可能掩盖真实的重渲染问题
进阶技巧:定位重渲染的"核武器"
使用why-did-you-render工具可以精准定位问题:
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, {
trackAllPureComponents: true,
});
它会告诉你哪些props变化导致了重渲染,甚至显示前后props的差异对比。有一次它帮我发现了一个被连续赋值3次的深层嵌套对象——这种问题靠人眼根本看不出来。
终极解法:不是所有问题都该用useMemo解决
性能优化是有代价的。经过多次实践,我总结出这样的优先级:
- 先设计合理的组件结构:把变化频繁的部分拆分成独立组件
- 利用children属性:
<Layout><Sidebar /></Layout>比<Layout sidebar={<Sidebar />} />更稳定 - 最后才用useMemo/useCallback:它们应该作为补救措施,而不是默认写法
"现在我能听出重渲染的声音了"
经历了这次调试,我现在甚至能通过肉眼观察Chrome性能面板的"JS堆"曲线,判断是否存在无效重渲染。React的性能问题往往不是框架的错,而是我们对它的更新机制理解不够深刻。
你在项目中是怎么处理重渲染问题的?有没有遇到过什么离奇案例?欢迎在评论区分享你的战斗经历。