You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ReactJS自动滚动优化:大量数据(500+条)下的性能提升方案

优化React自动循环滚动列表性能(支持千级数据量)

我正在开发一个列表自动循环滚动功能,例如数组[1,2,3,4,5,6,7,8,9],界面上从1到9自动滚动,显示9之后1接续循环。当前功能可正常运行,但数据量增大到500条以上时性能不佳,希望优化以支持1000条数据的流畅滚动。原代码如下:

import React, { Children, useEffect } from "react";
import { useMeasureChildren } from "./hooks/useMeasureChildren";
import { getChildrenHeight, getFrontItemHeight } from "./heplers";
import { FPS, SPEED } from "./constants";

const AutoScroll = ({ children, className, height, speed, fps }: any) => {
  const measureResults = useMeasureChildren(children.length);

  useEffect(() => {
    let globalOffsetY = 0;
    let offsetY = Array.from({ length: children.length }).map(() => 0);
    const childrenHeight = getChildrenHeight(measureResults);
    let handle: any;
    if (height < childrenHeight) {
      handle = setInterval(() => {
        globalOffsetY -= 0.1 * speed;
        if (globalOffsetY < -childrenHeight) {
          globalOffsetY += childrenHeight;
          offsetY = offsetY.map(() => 0);
        }
        measureResults &&
          measureResults.forEach(({ node }: any, idx) => {
            if (node) {
              if (
                globalOffsetY + offsetY[idx] <=
                -getFrontItemHeight(measureResults, idx)
              ) {
                offsetY[idx] = childrenHeight;
              }
              node.style.transform = `translateY(${globalOffsetY + offsetY[idx]}px)`;
            }
          });
      }, 1000 / (fps || FPS));
    }

    return () => {
      handle && clearInterval(handle);
    };
  }, [measureResults]);

  return (
    <div
      className={`${className} auto-scroll`}
      style={{
        height: `${height}px`,
        overflowY: !speed ? "auto" : "hidden",
      }}
    >
      {Children.map(children, (child, idx) => {
        return <div ref={measureResults[idx].measureRef}>{child}</div>;
      })}
    </div>
  );
};

export default AutoScroll;

原代码性能瓶颈

  1. DOM操作过载:每次定时器触发时遍历所有子元素更新transform,1000条数据意味着每次循环要操作1000个DOM节点,频繁触发重排重绘导致卡顿。
  2. 无虚拟滚动:所有子元素一次性渲染到DOM中,内存占用高,初始渲染时间长。
  3. 重复计算:每次循环调用getFrontItemHeight重复查询DOM高度,增加不必要的性能开销。
  4. setInterval局限性:定时器执行时机不与浏览器刷新同步,容易出现丢帧、滚动不流畅的问题。

优化方案

1. 虚拟滚动:只渲染可视区域内的元素

虚拟滚动是解决大数据列表性能问题的核心,它只渲染当前视口可见的元素,加上前后少量缓冲元素,把DOM节点数量从千级降到几十级。

2. 用requestAnimationFrame替代setInterval

requestAnimationFrame由浏览器刷新周期驱动(通常60FPS),能保证动画与页面刷新同步,避免丢帧,滚动更流畅。

3. 缓存元素高度,避免重复DOM查询

首次渲染后缓存所有子元素的高度,后续滚动时直接使用缓存值,减少DOM查询次数。

4. 容器偏移替代逐个元素移动

通过调整容器的内边距(padding)模拟滚动偏移,而非逐个修改子元素的transform,大幅减少DOM操作次数。

优化后的代码

import React, { useRef, useState, useEffect, useMemo } from "react";
import { FPS, SPEED } from "./constants";

// 可视区域外的缓冲元素数量,避免滚动时出现空白
const BUFFER_COUNT = 2;

const AutoScrollVirtual = ({ children, className, height, speed = SPEED, fps = FPS }) => {
  const containerRef = useRef(null);
  const itemHeightRef = useRef([]); // 缓存每个子元素的高度
  const [scrollOffset, setScrollOffset] = useState(0);
  const itemCount = children.length;
  const frameRef = useRef(null);

  // 缓存总高度,避免重复计算
  const totalHeight = useMemo(() => {
    return itemHeightRef.current.reduce((sum, h) => sum + h, 0);
  }, [itemCount]);

  // 计算可视区域能容纳的元素数量(加缓冲)
  const visibleItemCount = useMemo(() => {
    if (!height || !itemHeightRef.current[0]) return 0;
    return Math.ceil(height / itemHeightRef.current[0]) + BUFFER_COUNT * 2;
  }, [height, itemCount]);

  // 首次渲染后测量子元素高度(假设高度一致,不一致可逐个测量)
  useEffect(() => {
    const firstItem = containerRef.current?.querySelector(".scroll-item");
    if (firstItem) {
      const itemHeight = firstItem.offsetHeight;
      itemHeightRef.current = Array(itemCount).fill(itemHeight);
    }
  }, [itemCount]);

  // 自动滚动逻辑
  useEffect(() => {
    if (!containerRef.current || height >= totalHeight) return;

    // 计算每帧滚动的距离
    const scrollStep = (speed / 100) * (1000 / fps);
    let currentOffset = 0;

    const animate = () => {
      currentOffset -= scrollStep;
      // 循环重置偏移量,实现无缝滚动
      if (Math.abs(currentOffset) >= totalHeight) {
        currentOffset += totalHeight;
      }
      setScrollOffset(currentOffset);
      frameRef.current = requestAnimationFrame(animate);
    };

    frameRef.current = requestAnimationFrame(animate);

    // 清理动画帧
    return () => {
      frameRef.current && cancelAnimationFrame(frameRef.current);
    };
  }, [totalHeight, height, speed, fps]);

  // 计算需要渲染的元素起始索引
  const startIndex = useMemo(() => {
    if (totalHeight === 0) return 0;
    let currentSum = 0;
    let index = 0;
    while (currentSum > scrollOffset && index < itemCount) {
      currentSum -= itemHeightRef.current[index];
      index++;
    }
    return Math.max(0, index - BUFFER_COUNT);
  }, [scrollOffset, totalHeight]);

  // 计算需要渲染的元素结束索引
  const endIndex = useMemo(() => {
    return Math.min(itemCount - 1, startIndex + visibleItemCount);
  }, [startIndex, visibleItemCount]);

  // 计算容器的paddingTop,模拟滚动偏移
  const paddingTop = useMemo(() => {
    let sum = 0;
    for (let i = 0; i < startIndex; i++) {
      sum += itemHeightRef.current[i];
    }
    return sum + scrollOffset;
  }, [startIndex, scrollOffset]);

  return (
    <div
      className={`${className} auto-scroll`}
      style={{
        height: `${height}px`,
        overflow: "hidden",
        position: "relative",
      }}
      ref={containerRef}
    >
      <div
        style={{
          paddingTop: `${paddingTop}px`,
          paddingBottom: `${totalHeight - (paddingTop - scrollOffset)}px`,
        }}
      >
        {Array.from({ length: endIndex - startIndex + 1 }).map((_, idx) => {
          const realIndex = startIndex + idx;
          return (
            <div key={realIndex} className="scroll-item">
              {children[realIndex]}
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default AutoScrollVirtual;

额外优化建议

  • 如果子元素高度不一致,可在渲染时逐个测量并更新itemHeightRef.current,同时调整虚拟滚动的索引计算逻辑。
  • 使用React.memo包裹子组件,避免子组件因父组件重渲染而不必要更新。
  • 若滚动速度固定,可将scrollStep改为常量,减少每次渲染的计算开销。

内容的提问来源于stack exchange,提问作者Greg Nguyen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 08:54:56