虚拟滚动时为什么还是会有重复渲染和卡顿?

Good“增芳 阅读 52

我在用虚拟滚动渲染长列表时,虽然实现了可视区域截取,但滚动到列表中段时偶尔会出现重复渲染的列表项,甚至卡顿一下。尝试用IntersectionObserver监听可视区域,但发现当快速滚动时计算的startIndexendIndex会有偏差。

代码大概是这样写的:

const observer = new IntersectionObserver(entries => {
  const rect = entries[0].boundingClientRect;
  const start = Math.floor(rect.top / itemHeight);
  const end = Math.ceil((rect.bottom + window.innerHeight) / itemHeight);
  setRenderRange({ start, end }); // 这里可能有问题?
});
observer.observe(ref.current);

但发现当列表项高度不固定时,计算出来的范围会错位,导致渲染区域重叠。有没有更好的可视区域计算方式?

我来解答 赞 3 收藏
二维码
手机扫码查看
2 条解答
程序猿雯雯
你这个问题主要是因为快速滚动时 IntersectionObserver 的回调触发时机和计算逻辑不够精准,特别是列表项高度不固定的情况下。建议用更稳定的滚动容器监听方式,结合 scrollTop 和 clientHeight 来动态计算渲染范围。

代码可以改成这样:
const handleScroll = () => {
const scrollTop = containerRef.current.scrollTop;
const start = Math.floor(scrollTop / estimatedItemHeight);
const end = Math.ceil((scrollTop + containerRef.current.clientHeight) / estimatedItemHeight);
setRenderRange({ start, end });
};

containerRef.current.addEventListener('scroll', handleScroll, { passive: true });


记得用一个预估的 estimatedItemHeight 来处理高度不固定的场景,避免计算偏差。如果高度差异太大,可以在数据里加一个缓存记录每个项的真实高度,动态调整计算结果。别忘了在组件销毁时清理监听器。
点赞
2026-02-19 21:01
公孙国娟
虚拟滚动的核心就是精确计算可视区域,你的问题出在高度不固定和快速滚动时的偏差。用 IntersectionObserver 不是最佳选择,尤其当列表项高度动态变化时,计算会越来越不准。试试下面这种基于滚动容器的方案,直接监听滚动事件来动态调整渲染范围。

import React, { useRef, useState, useEffect } from 'react';

function VirtualList({ itemCount, itemHeightGetter, renderItem }) {
const containerRef = useRef(null);
const [renderRange, setRenderRange] = useState({ start: 0, end: 0 });
const [scrollTop, setScrollTop] = useState(0);

useEffect(() => {
const container = containerRef.current;
if (!container) return;

const handleScroll = () => {
const scrollTop = container.scrollTop;
setScrollTop(scrollTop);

// 动态计算当前可视区域
let startIndex = 0;
let endIndex = 0;
let accumulatedHeight = 0;

for (let i = 0; i < itemCount; i++) {
const height = itemHeightGetter(i);
if (accumulatedHeight + height > scrollTop) {
startIndex = i;
break;
}
accumulatedHeight += height;
}

accumulatedHeight = 0;
for (let i = 0; i < itemCount; i++) {
const height = itemHeightGetter(i);
accumulatedHeight += height;
if (accumulatedHeight > scrollTop + container.clientHeight) {
endIndex = i;
break;
}
}

setRenderRange({ start: Math.max(0, startIndex - 5), end: Math.min(itemCount - 1, endIndex + 5) });
};

container.addEventListener('scroll', handleScroll);
handleScroll(); // 初始化触发一次
return () => container.removeEventListener('scroll', handleScroll);
}, [itemCount, itemHeightGetter]);

return (
ref={containerRef}
style={{ overflowY: 'auto', height: '100%', position: 'relative' }}
>
acc + itemHeightGetter(i), 0) }} />
{Array.from({ length: renderRange.end - renderRange.start + 1 }).map((_, index) => {
const itemIndex = renderRange.start + index;
return (
key={itemIndex}
style={{
position: 'absolute',
top: Array.from({ length: itemIndex }).reduce((acc, _, i) => acc + itemHeightGetter(i), 0),
height: itemHeightGetter(itemIndex),
}}
>
{renderItem(itemIndex)}

);
})}

);
}

// 使用示例
function App() {
const itemCount = 1000;
const itemHeights = Array.from({ length: itemCount }, () => Math.floor(Math.random() * 50 + 50));

const getItemHeight = (index) => itemHeights[index];
const renderItem = (index) => Item ${index};

return ;
}


这个代码解决了两个核心问题:一是支持动态高度,通过累加计算每个元素的位置;二是优化了快速滚动时的偏差,增加了一个缓冲区(startIndex - 5endIndex + 5)。复制过去试试,应该能解决你的重复渲染和卡顿问题。如果还有问题再交流。
点赞
2026-02-18 21:02