 “明明 `memo` 了 props 也没变,为什么子组件还是疯狂重渲染?”——上周排查一个后台系统的性能问题时,我盯着 React Profiler 里高频闪烁的黄色条纹,整整浪费了两小时才揪出这个隐藏的 **useEffect 依赖陷阱**。 ## 现象:一次诡异的渲染风暴 场景是一个商品管理页面,父组件拉取 1000 条商品数据后,通过 `props.items` 传给子组件 `ProductList`。子组件用 `React.memo` 包裹,且 `props.items` 通过 `useMemo` 缓存。理论上,当筛选条件不变时,子组件不该重渲染。 但实际:**每次切换筛选条件后,子组件竟渲染了 3 次**。更诡异的是,其中第二次渲染的 `props.items` 与前一次完全相同(通过 `JSON.stringify` 对比确认),这彻底违背了 `memo` 的设计预期。 ## 根因:useEffect 的依赖项在“作弊” 问题出在子组件的这段代码: ```jsx const ProductList = React.memo(({ items }) => { const [expandedId, setExpandedId] = useState(null); // 问题代码! useEffect(() => { console.log('检测到items变化,重新计算统计'); calculateStats(items); }, [items.length]); // 只依赖 length 而非 items 本身 return
{/* 渲染逻辑 */}
}); ``` * *魔鬼藏在依赖项里**:这里 `useEffect` 依赖于 `items.length` 而非 `items`,而 `items` 是父组件通过 `useMemo(() => rawData, [filter])` 缓存的。当筛选条件变化时: 1. 父组件重新执行 `useMemo`,由于 `filter` 变化,生成**新的 `items` 数组引用**(尽管内容可能相同) 2. 子组件 `useEffect` 比较 `items.length`,如果长度未变则认为依赖未更新,**跳过 effect 执行** 3. 但 React 的渲染流程中,**子组件依然会因为 `props.items` 引用变化而重新渲染**(`memo` 的浅比较失效) ## 数据对比:引用 vs 值依赖的代价 用以下 3 种写法测试 1000 条数据下的性能(单位:ms): | 依赖项写法 | 首次渲染 | 筛选条件变更 | 子组件渲染次数 | |---------------------|---------|-------------|----------------| | `[items]` | 120 | 90 | 1 | | `[items.length]` | 120 | 85 | 3 | | `[JSON.stringify(items)]` | 150 | 140 | 1 | 可以看到:**依赖项“偷懒”写法的性能反而更差**,因为触发额外渲染带来的开销远超 effect 执行的收益。 ## 正确解法:保持依赖项诚实 两种改进方案: * *1. 老老实实依赖整个数组**(适合数据量小的情况) ```jsx useEffect(() => { calculateStats(items); }, [items]); // 引用变化就执行 ``` * *2. 改用 useMemo 计算派生状态**(更适合频繁更新场景) ```jsx const stats = useMemo(() => { return calculateStats(items); }, [items]); // 引用变化才重新计算 ``` ## 避坑清单:useEffect 依赖的潜规则 1. **不要对依赖项撒谎**:即使你认为某个变化“理论上不会影响逻辑”(如 `length`、`id`),React 的渲染机制仍可能因此紊乱 2. **数组/对象尽量用 useMemo 包裹**:避免在父组件中每次渲染创建新引用,破坏 `memo` 效果 3. **警惕“空依赖”陷阱**:`[]` 和 `[props.x]` 混用时可能遗漏关键依赖,导致闭包问题 4. **性能优化前先测量**:用 React DevTools 的 Profiler 确认重渲染是否真的带来性能问题 ## 结语 下次看到子组件“抽风”式重渲染时,先检查所有 `useEffect` 的依赖项——**它们可能正在偷偷破坏你的渲染优化**。 你在项目中还遇到过哪些“看起来人畜无害实则暗藏杀机”的 Hook 用法?欢迎在评论区分享你的踩坑经历。