React重渲染这坑,我跳进去又爬出来了

31 阅读1分钟

"为什么我的表格又卡了?"

去年做数据中台项目时,我遇到一个诡异现象:一个渲染500行数据的Ant Design表格,每次勾选某行checkbox,整个页面都会卡顿近1秒。更离谱的是,我用React.memo包裹了每一行组件,理论上不该重渲染——你遇到过这种"明明用了优化手段却依然性能崩盘"的情况吗?

重渲染的底层逻辑:你以为的"没变"可能已经变了

问题的根源在于React的引用相等性检查机制React.memouseMemo确实会比对前后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后,性能数据差异惊人:

操作优化前耗时优化后耗时
勾选单行checkbox800ms30ms
切换页码1200ms200ms

关键修改点:

// 正确写法:用useMemo保持引用稳定
const rowSelection = useMemo(() => ({
  selectedRowKeys,
  onChange: (keys) => setSelectedRowKeys(keys)
}), [selectedRowKeys]);  // 依赖项要精确

避坑清单:这些重渲染陷阱你踩过几个?

  1. 内联对象/函数<Child style={{ color: 'red' }} />这种写法每次都会创建新对象
  2. 错误的使用Context:如果Context Provider的value没有用useMemo包裹,任何context变化会导致所有消费者重渲染
  3. 数组索引作为key:当列表顺序变化时,会导致React误判元素关系,触发不必要的DOM操作
  4. 过度使用状态提升:把本应属于子组件的状态提升到父组件,会导致父组件频繁重渲染
  5. 忽略React.StrictMode:开发环境下它会故意双渲染组件,可能掩盖真实的重渲染问题

进阶技巧:定位重渲染的"核武器"

使用why-did-you-render工具可以精准定位问题:

import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, {
  trackAllPureComponents: true,
});

它会告诉你哪些props变化导致了重渲染,甚至显示前后props的差异对比。有一次它帮我发现了一个被连续赋值3次的深层嵌套对象——这种问题靠人眼根本看不出来。

终极解法:不是所有问题都该用useMemo解决

性能优化是有代价的。经过多次实践,我总结出这样的优先级:

  1. 先设计合理的组件结构:把变化频繁的部分拆分成独立组件
  2. 利用children属性<Layout><Sidebar /></Layout><Layout sidebar={<Sidebar />} />更稳定
  3. 最后才用useMemo/useCallback:它们应该作为补救措施,而不是默认写法

"现在我能听出重渲染的声音了"

经历了这次调试,我现在甚至能通过肉眼观察Chrome性能面板的"JS堆"曲线,判断是否存在无效重渲染。React的性能问题往往不是框架的错,而是我们对它的更新机制理解不够深刻

你在项目中是怎么处理重渲染问题的?有没有遇到过什么离奇案例?欢迎在评论区分享你的战斗经历。