react-虚拟列表解决方案

59 阅读2分钟

        在近日的开发中遇到了一次渲染大量数据,导致渲染缓慢的问题,数据量大是第一点,第二点是使用了antd的Typography中的Text去进行展示Text组件包含ellipsis去控制文本过长时展示tooltip。

        Ant Design 的 Text 组件的 ellipsis 属性不是简单使用 CSS 的 text-overflow: ellipsis,而是采用了复杂的 JavaScript 实现。

  • 二分查找算法:使用二分搜索来确定截断位置,需要不断测量文本高度

  • 动态测量:需要创建隐藏的测量元素来计算文本尺寸

  • 混合测量法:结合 CSS 和 JS 来处理复杂的布局情况

另外Text组件的 ellipsis 实现,需要动态创建测量用的 span 元素,反复计算元素的 clientHeight,多次 DOM 重排和重绘以及处理 tooltip 的显示逻辑等等所以大量的Text组件渲染会导致渲染速度慢,

解决方案:1可以尝试用普通css代替完成效果,可以减去很多额外的计算。不需要测量和重新计算,可以用GPU帮忙计算等等

2,使用虚拟化滚动创建虚拟列表

npm install rc-virtual-list

import VirtualList from "rc-virtual-list";

// 在 CloudStorage 组件中添加以下代码:

const CloudStorage = () => {
  // 现有的 state...
  
  // 新增:容器高度和每项高度配置
  const folderContainerRef = useRef<HTMLDivElement>(null);
  const FOLDER_ITEM_HEIGHT = 120; // FolderCard 高度 + margin
  const CONTAINER_HEIGHT = 600; // 或动态计算

  // 计算容器高度(可选,根据实际需求调整)
  const containerHeight = folderContainerRef.current
    ? folderContainerRef.current.clientHeight
    : CONTAINER_HEIGHT;

  // 渲染虚拟化的文件夹列表
  const renderVirtualFolderList = () => {
    if (folderList.length === 0) return null;

    return (
      <div 
        className="folder-list-container" 
        ref={folderContainerRef}
        style={{ height: containerHeight }}
      >
        <VirtualList
          data={folderList}
          height={containerHeight}
          itemHeight={FOLDER_ITEM_HEIGHT}
          itemKey="uuid"
        >
          {(item, index) => (
            <div
              key={item.uuid}
              style={{
                padding: "10px",
                display: "inline-block",
                width: "calc(25% - 20px)", // 4列布局,可根据需要调整
                marginRight: "20px",
                marginBottom: "20px",
              }}
            >
              <FolderCard
                data={{
                  id: item.uuid,
                  icn:
                    item?.directory?.directory_category === 3 ||
                    item?.directory?.directory_category === 7
                      ? require("@/assets/img/icn_file_share.png")
                      : require("@/assets/img/icn_file.png"),
                  title: item.name,
                  content: item?.directory?.directory_desc,
                }}
                disabledHandler={
                  item.directory?.directory_category === 6 ||
                  item.directory?.directory_category === 7
                }
                onDoubleClick={() => {
                  const current = {
                    uuid: item.uuid,
                    name: item.name,
                    directory_category: item?.directory?.directory_category,
                    directory_level: item?.directory?.directory_level,
                    permission: item.permission,
                  };
                  setBreadcrumb([...breadcrumb, current]);
                  setCurrentPathData(current);
                }}
                handlerOptions={calcHandlerOptions(item)}
                onClickHandler={(key: any) => onClickHandler(key, item)}
              />
            </div>
          )}
        </VirtualList>
      </div>
    );
  };

  // 在原来的渲染位置替换:
  return (
    <div>
      {/* 其他代码... */}
      
      {folderList.length > 0 && (
        <>
          <Flex justify="space-between">
            <div className="font-bold">文件夹</div>
            <FilterSortSelect
              options={[
                { value: "created_at", label: "按创建时间" },
                { value: "target_size", label: "按文件大小" },
              ]}
              value={folerFilterValue}
              onChange={(value: any) => setFolderFilterValue(value)}
            />
          </Flex>
          
          {/* 替换原来的 Space wrap */}
          {renderVirtualFolderList()}
        </>
      )}
      
      {/* 其他代码... */}
    </div>
  );
};

或者使用

import VirtualList from "rc-virtual-list";

const CloudStorage = () => {
  // 网格配置
  const COLUMNS_PER_ROW = 4; // 每行显示的列数
  const FOLDER_CARD_WIDTH = 200; // 每个卡片的宽度
  const FOLDER_CARD_HEIGHT = 100; // 每个卡片的高度
  const ROW_HEIGHT = FOLDER_CARD_HEIGHT + 20; // 行高(包含间距)

  // 将一维数组转换为二维数组(按行分组)
  const groupedFolderList = useMemo(() => {
    const groups = [];
    for (let i = 0; i < folderList.length; i += COLUMNS_PER_ROW) {
      groups.push(folderList.slice(i, i + COLUMNS_PER_ROW));
    }
    return groups;
  }, [folderList]);

  const renderVirtualGridFolderList = () => {
    if (groupedFolderList.length === 0) return null;

    return (
      <div 
        className="folder-grid-container"
        style={{ height: 600, width: "100%" }}
      >
        <VirtualList
          data={groupedFolderList}
          height={600}
          itemHeight={ROW_HEIGHT}
          itemKey={(item, index) => `row-${index}`}
        >
          {(rowItems, rowIndex) => (
            <div
              key={`row-${rowIndex}`}
              style={{
                display: "flex",
                gap: "20px",
                padding: "10px 0",
                height: ROW_HEIGHT,
              }}
            >
              {rowItems.map((item: any) => (
                <FolderCard
                  key={item.uuid}
                  data={{
                    id: item.uuid,
                    icn:
                      item?.directory?.directory_category === 3 ||
                      item?.directory?.directory_category === 7
                        ? require("@/assets/img/icn_file_share.png")
                        : require("@/assets/img/icn_file.png"),
                    title: item.name,
                    content: item?.directory?.directory_desc,
                  }}
                  disabledHandler={
                    item.directory?.directory_category === 6 ||
                    item.directory?.directory_category === 7
                  }
                  onDoubleClick={() => {
                    const current = {
                      uuid: item.uuid,
                      name: item.name,
                      directory_category: item?.directory?.directory_category,
                      directory_level: item?.directory?.directory_level,
                      permission: item.permission,
                    };
                    setBreadcrumb([...breadcrumb, current]);
                    setCurrentPathData(current);
                  }}
                  handlerOptions={calcHandlerOptions(item)}
                  onClickHandler={(key: any) => onClickHandler(key, item)}
                />
              ))}
            </div>
          )}
        </VirtualList>
      </div>
    );
  };

  // 使用方式相同...
};