React中用requestIdleCallback优化长列表还是卡顿怎么办?

程序员思捷 阅读 24

我在用React实现一个动态加载的长列表,尝试用requestIdleCallback分批次渲染列表项,但滚动到后面还是会卡顿。我按网上的方法在useEffect里这样写的:


const [items, setItems] = useState([]);
useEffect(() => {
  let offset = 0;
  const renderBatch = () => {
    requestIdleCallback(() => {
      const batch = generateItems(50, offset);
      setItems(prev => [...prev, ...batch]);
      offset += 50;
      return offset < totalItems ? renderBatch() : undefined;
    });
  };
  renderBatch();
}, []);

但测试时发现:1. 滚动到第100项后开始明显卡顿 2. 滑动停止后界面有0.5秒延迟渲染。是不是requestIdleCallback的使用时机有问题?或者需要配合其他优化手段?

我来解答 赞 3 收藏
二维码
手机扫码查看
2 条解答
爱学习的杰森
你的写法确实容易卡。requestIdleCallback 的回调优先级太低,等屏幕静止才渲染,但你滚动时它会积压任务,导致滑停后才突然加载,造成你说的0.5秒延迟。

根本问题是:你不该等空闲才渲染,而应该在滚动时按帧切片渲染。React 的并发模式(v18+)已经支持时间切片,但你可以手动模拟。

我建议你改成 setTimeout + useRef 控制渲染批次,这样更可控:

const [items, setItems] = useState([]);
const offsetRef = useRef(0);
const timerRef = useRef(null);

useEffect(() => {
const renderNextBatch = () => {
if (offsetRef.current >= totalItems) return;

const batch = generateItems(20, offsetRef.current);
setItems(prev => [...prev, ...batch]);
offsetRef.current += 20;

timerRef.current = setTimeout(() => {
requestAnimationFrame(renderNextBatch);
}, 0);
};

requestAnimationFrame(renderNextBatch);

return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);


这样每帧只渲染一小批,不会长时间阻塞主线程,滚动更顺。而且你不该一口气加载到尾,应该监听滚动事件,只加载当前视口附近的项,不然数据量一大照样卡。

建议加个虚拟滚动(如 react-window),只渲染可见区域的列表项,性能能提升一个档次。
点赞 5
2026-02-05 20:14
端木梓睿
你的代码逻辑大致是对的,但 requestIdleCallback 本身不是万能的,它只是尽可能利用空闲时间做事情,不保证执行时间。React 的渲染本身是同步的,即使你分批次更新状态,每个批次还是会导致一次 re-render,当列表项过多时,组件树过深依然会卡顿。

问题可能出在两个地方:一是 DOM 节点数量没控制,二是 setState 的频率和渲染压力没控制好。

解决方法可以试试这些:

使用 windowing 技术(虚拟滚动),比如 react-windowreact-virtualized,只渲染可视区域附近的元素,大大减少 DOM 节点数量。

如果你想继续用 requestIdleCallback,可以加上一个“优先级”判断,例如用户正在滚动时暂停加载,等滚动结束再恢复。可以通过 requestIdleCallbackdeadline 参数判断当前帧是否有空闲时间。

使用 useMemoReact.memo 减少重复渲染,确保列表项的 props 不变时复用渲染结果。

setTimeout 做懒加载兜底,如果 requestIdleCallback 长时间没执行,可以 fallback 到定时器。

这是个简化版思路:

const [items, setItems] = useState([]);
const [isScrolling, setIsScrolling] = useState(false);

useEffect(() => {
let offset = 0;
const renderBatch = () => {
const process = () => {
const batch = generateItems(50, offset);
setItems(prev => [...prev, ...batch]);
offset += 50;
if (offset < totalItems) {
requestIdleCallback(process);
}
};
requestIdleCallback(process);
};
renderBatch();
}, []);

useEffect(() => {
const handleScroll = () => {
setIsScrolling(true);
clearTimeout(window._scrollTimer);
window._scrollTimer = setTimeout(() => {
setIsScrolling(false);
}, 150);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
点赞 6
2026-02-05 16:28