Tree树形组件的那些坑我帮你踩过了

卿硕的笔记 组件 阅读 736
赞 9 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

最近重构了一个后台管理系统的树形组件,真的是被性能问题折磨得够呛。那个树大概有3000多个节点,嵌套七八层深度,优化前每次渲染都要5秒多,展开收起更是卡得鼠标都动不了。用户反馈说点击展开按钮要等好几秒才反应,体验差到爆。

Tree树形组件的那些坑我帮你踩过了

找到病根了!

Chrome DevTools一打开就发现问题所在:DOM节点太多了,一次性渲染了3000多个

  • 标签,内存占用直接飙到200MB+。React DevTools显示每次状态更新都要重新render整个tree,虚拟DOM diff也成了负担。

    懒加载 + 虚拟滚动双管齐下

    试了几种方案,最后用懒加载和虚拟滚动结合的方式效果最好。先看优化前的代码:

    // 优化前 - 全量渲染
    function OldTree({ data }) {
      return (
        <ul className="tree">
          {data.map(node => (
            <li key={node.id}>
              <div>{node.name}</div>
              {node.children && node.expanded && (
                <OldTree data={node.children} />
              )}
            </li>
          ))}
        </ul>
      );
    }

    这种写法在数据量大时就是灾难,每个节点都生成DOM元素。改成懒加载后:

    // 优化后 - 懒加载 + 虚拟滚动
    function OptimizedTree({ 
      data, 
      expandedNodes = new Set(), 
      onToggle,
      itemHeight = 32 
    }) {
      // 只渲染可视区域的节点
      const visibleNodes = useMemo(() => {
        let result = [];
        
        function traverse(nodes, depth = 0) {
          nodes.forEach(node => {
            if (depth === 0 || expandedNodes.has(node.id)) {
              result.push({ ...node, depth });
              
              if (node.children && expandedNodes.has(node.id)) {
                traverse(node.children, depth + 1);
              }
            }
          });
        }
        
        traverse(data);
        return result;
      }, [data, expandedNodes]);
    
      // 虚拟滚动计算
      const [scrollTop, setScrollTop] = useState(0);
      const containerHeight = 400; // 容器高度
      const startIndex = Math.floor(scrollTop / itemHeight);
      const endIndex = Math.min(
        startIndex + Math.ceil(containerHeight / itemHeight) + 5, // 多渲染几个做缓冲
        visibleNodes.length
      );
      
      const renderedNodes = visibleNodes.slice(startIndex, endIndex);
    
      return (
        <div 
          className="tree-container"
          onScroll={(e) => setScrollTop(e.target.scrollTop)}
          style={{ height: containerHeight, overflowY: 'auto' }}
        >
          <div 
            style={{ 
              height: visibleNodes.length * itemHeight,
              position: 'relative'
            }}
          >
            {renderedNodes.map((node, index) => {
              const actualIndex = startIndex + index;
              return (
                <TreeNode
                  key={node.id}
                  node={node}
                  depth={node.depth}
                  top={actualIndex * itemHeight}
                  onToggle={onToggle}
                  isExpanded={expandedNodes.has(node.id)}
                />
              );
            })}
          </div>
        </div>
      );
    }
    
    function TreeNode({ node, depth, top, onToggle, isExpanded }) {
      return (
        <div
          style={{
            position: 'absolute',
            top: ${top}px,
            left: ${depth * 20}px,
            width: calc(100% - ${depth * 20}px),
            height: '32px',
            display: 'flex',
            alignItems: 'center'
          }}
        >
          {node.children && (
            <button 
              onClick={() => onToggle(node.id)}
              style={{ marginRight: '8px' }}
            >
              {isExpanded ? '-' : '+'}
            </button>
          )}
          <span>{node.name}</span>
        </div>
      );
    }

    这里有个关键点是visibleNodes的计算,只在expandedNodes变化时重新计算,避免每次都遍历整个树。另外itemHeight要固定,这样才能准确计算可视区域。

    节点缓存优化

    光有虚拟滚动还不够,组件频繁重渲染也是个问题。给TreeNode加memo:

    const MemoizedTreeNode = memo(TreeNode, (prev, next) => {
      return prev.node.id === next.node.id &&
             prev.top === next.top &&
             prev.isExpanded === next.isExpanded &&
             prev.depth === next.depth;
    });

    这样只有真正需要更新的节点才会重渲染。

    异步加载子节点

    对于超大数据量,还可以按需加载子节点。把children设置为函数:

    // 初始数据结构
    const initialData = [
      {
        id: 1,
        name: '父节点',
        hasChildren: true, // 标记是否有子节点
        loadChildren: async () => {
          // 异步获取子节点数据
          const response = await fetch(https://jztheme.com/api/tree/${id}/children);
          return response.json();
        }
      }
    ];
    
    // 展开时才加载子节点
    const handleToggle = async (nodeId) => {
      const node = findNode(nodeId);
      if (node.hasChildren && !node.children && node.loadChildren) {
        const children = await node.loadChildren();
        updateNode(nodeId, { children });
      }
      
      toggleExpanded(nodeId);
    };

    性能数据对比

    优化前后对比明显:渲染时间从5.2秒降到800毫秒,内存占用从200MB降到15MB左右,展开收起响应时间从3秒降到100毫秒以内。用户反馈明显改善,再也没有卡顿投诉了。

    需要注意的是,虚拟滚动的itemHeight一定要固定,不然计算位置会有偏差。还有就是事件处理要考虑冒泡问题,建议统一在容器上处理。

    以上是我的优化经验,有更好的方案欢迎交流

    这套优化方案虽然增加了复杂度,但性能提升还是很明显的。特别是那种层级深、节点多的场景,效果特别好。如果你也有类似的树形组件性能问题,可以试试这个方案。

  • 本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
    发表评论

    暂无评论