页面滚动一卡一卡的,是性能瓶颈?DOM 结构太复杂?IntersectionObserver 用错了?我也这么想过……直到我发现罪魁祸首是:高频率的 setState 调用。
本文分享一次真实的 React 性能排查过程,揭示一个常被忽视却致命的问题,并给出清晰的优化建议。适合正在做仪表盘、大列表滚动的你参考。
问题现象:图表一多,滚动就“卡成 PPT”
我们的页面结构非常常见:多个图表组件组成一个仪表盘页面,用户通过滚动浏览全部内容。
const Dashboard = () => {
const [graphs, setGraphs] = useState([]);
const [graphsVisibleMap, setGraphsVisibleMap] = useState({});
return (
<div>
{graphs.map((graph) => (
<GraphComponent
key={graph.id}
data={graph}
onVisibilityChange={handleGraphVisibleChange}
/>
))}
</div>
);
};
问题: 图表数量稍多(超过 20 个)后,滚动操作变得极其卡顿,连滚动条都不丝滑了。
初步推理:怀疑是 IntersectionObserver 惹的祸
使用了 IntersectionObserver
来监听每个图表是否在视口中,决定是否触发数据加载。
setupIntersectionObserver() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const graphId = entry.target.getAttribute('data-graph-id');
if (graphId) {
this.props.onGraphVisibleChange(Number(graphId), entry.isIntersecting);
}
});
}, {
threshold: 0.1
});
}
因为可见性在滚动时频繁变化,怀疑是 Observer 的回调频繁触发,导致主线程阻塞。
❌优化误区:尝试了这些“看似正确”的方法
尝试 1:提升触发门槛
threshold: 0.5,
rootMargin: '0px',
减少回调触发频率,没啥明显效果。
尝试 2:回调加防抖
_.debounce((entries) => {
// 处理逻辑
}, 100);
也无效,因为滚动时变化太频繁,节流无法兜住。
尝试 3:批量处理变化
const visibilityBuffer = new Map();
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const id = entry.target.getAttribute('data-graph-id');
visibilityBuffer.set(id, entry.isIntersecting);
});
setTimeout(() => {
this.processBatchedUpdates(visibilityBuffer);
}, 50);
});
依旧卡顿,说明根源不在 Observer 本身。
真正的问题:setState 的滥用
在图表组件中,调用了如下方法:
handleGraphVisibleChange = (id, visible) => {
const newMap = { ...this.state.graphsVisibleMap };
newMap[id] = visible;
this.setState({ graphsVisibleMap: newMap }); // 滚动时频繁触发
};
关键问题:
- 每次可见性变化都调用
setState
- 滚动时每秒触发 几十甚至上百次
- 每次都导致组件重渲染,主线程频繁阻塞
- 最终导致滚动卡顿甚至页面假死
最终方案:把“高频数据”移出 state!
如果某些数据只是用于临时逻辑判断、并不影响 UI 渲染,那就不应该放进 React 的 state
中。
将 graphsVisibleMap
从 state
移到了组件的实例属性中:
class Dashboard extends React.Component {
constructor() {
super();
this.graphsVisibleMap = {}; // 实例属性,非响应式
}
handleGraphVisibleChange = (id, visible) => {
this.graphsVisibleMap[id] = visible; // 不再触发 setState
};
render() {
return (
<div>
{this.props.graphs.map((graph) => (
<GraphComponent
key={graph.id}
data={graph}
onVisibilityChange={this.handleGraphVisibleChange}
/>
))}
</div>
);
}
}
结果:页面滚动顺滑,性能恢复,问题彻底解决!
总结:不是所有数据都需要放在 state 中!
类型 | 是否放入 state | 是否引发重渲染 | 示例 |
---|---|---|---|
影响 UI 的数据 | ✅ 必须放 | ✅ 会触发 | 图表数据、用户输入 |
高频变化但不影响 UI 的逻辑状态 | ❌ 不应该放 | ❌ 不触发 | 元素可见性、滚动缓存 |
你是否也遇到过类似的性能问题?欢迎在评论区聊聊你踩过的坑,或者有其他优化建议也欢迎提出!