React列表滚动时性能分析显示大量重排怎么解决?
我在用React开发无限加载列表时,发现滚动特别卡顿。用Chrome性能分析录屏后,发现Paint和Layout占比特别高,但列表组件已经用了useCallback和memo。
代码结构是这样的:
function InfiniteList({ items }) {
const ref = useRef();
useEffect(() => {
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
onLoadMore(); // 这里会触发数据更新
}
});
observer.observe(ref.current);
return () => observer.disconnect(); // 我加了清理函数
}, [items]); // 这里的依赖项对吗?
return (
<div>
{items.map(item => (
<div key={item.id} style={{ height: '100px' }}>
{item.content}
</div>
))}
<div ref={ref}>加载更多</div>
</div>
);
}
尝试过在组件外层加
强制滚动容器,但效果不明显。性能面板里Layout每次滚动都触发,是不是因为高度计算的问题?应该怎么优化呢?
你遇到的问题十有八九是由于虚拟滚动没做导致的。即使你用了memo和useCallback,但列表长了以后,浏览器还是要计算每一个节点的位置,特别是你在滚动的时候频繁触发Layout。
我当时也卡在这,反复检查useEffect依赖项,结果发现根本不是React的问题,而是渲染的DOM节点太多了。
最直接的解决办法是引入虚拟滚动(Virtual Scrolling),只渲染可视区域附近的元素。react-window是我用过最好使的方案,它不会一次性渲染所有元素,而是只保留当前可视区域加上一点点缓冲区的节点,性能提升非常明显。
下面是你代码的一个改造版本,用的是react-window:
这样改完你会发现Layout的频率明显下降,Paint也轻松很多。
还有一个点:你useEffect的依赖项是items,但如果你每次加载更多数据时都更新items,那么useEffect会频繁执行,这会导致不断创建新的IntersectionObserver实例。你可以用ref来缓存数据长度,避免频繁触发observer.observe。
有问题再来问我。