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

如何虚拟化含千余TreeItems的MUI TreeView以精简DOM?

MUI TreeView 千级节点虚拟化优化方案

核心思路

虚拟化的核心是只渲染当前视口内的节点,通过计算可见区域动态生成DOM,避免一次性渲染全部1000+节点,从而解决DOM过多导致的性能问题。以下是两种简便可行的实现方式:

方法一:基于 react-window 实现虚拟化

react-window 是轻量的虚拟化库,和MUI TreeView适配性较好,步骤如下:

1. 安装依赖

npm install react-window
# 或
yarn add react-window

2. 封装虚拟化Tree组件

import React, { useState, useMemo } from 'react';
import { TreeView, TreeItem } from '@mui/lab';
import { FixedSizeList as List } from 'react-window';

// 递归将树形结构转为扁平化列表(附带层级信息)
const flattenTree = (nodes, depth = 0) => {
  let result = [];
  nodes.forEach(node => {
    result.push({ ...node, depth });
    if (node.children) {
      result = [...result, ...flattenTree(node.children, depth + 1)];
    }
  });
  return result;
};

const VirtualizedTreeView = ({ nodes }) => {
  const [expanded, setExpanded] = useState([]);
  const flatNodes = useMemo(() => flattenTree(nodes), [nodes]);

  // 过滤出当前展开状态下需要显示的节点
  const visibleNodes = useMemo(() => {
    return flatNodes.filter(node => {
      if (node.depth === 0) return true;
      // 验证所有父节点是否处于展开状态
      const parentId = node.id.split('.').slice(0, -1).join('.');
      return expanded.includes(parentId);
    });
  }, [flatNodes, expanded]);

  const handleExpand = (event, nodeIds) => {
    setExpanded(nodeIds);
  };

  const renderTreeItem = ({ index, style }) => {
    const node = visibleNodes[index];
    return (
      <div style={{ ...style, paddingLeft: `${node.depth * 24}px` }}>
        <TreeItem
          nodeId={node.id}
          label={node.label}
        />
      </div>
    );
  };

  return (
    <TreeView
      expanded={expanded}
      onExpand={handleExpand}
      onCollapse={handleExpand}
      style={{ height: 400, overflow: 'auto' }}
    >
      <List
        height={400}
        itemCount={visibleNodes.length}
        itemSize={40} // 固定每个TreeItem的高度
        width="100%"
      >
        {renderTreeItem}
      </List>
    </TreeView>
  );
};

// 使用示例:生成1000个模拟节点
const App = () => {
  const generateNodes = () => {
    const nodes = [];
    for (let i = 0; i < 100; i++) {
      nodes.push({
        id: `node-${i}`,
        label: `节点 ${i}`,
        children: Array.from({ length: 10 }, (_, j) => ({
          id: `node-${i}-${j}`,
          label: `子节点 ${i}-${j}`
        }))
      });
    }
    return nodes;
  };

  return <VirtualizedTreeView nodes={generateNodes()} />;
};

export default App;

关键说明

  • 先将树形结构扁平化,便于react-window处理列表式渲染
  • 通过visibleNodes过滤出当前展开状态下需要显示的节点
  • 用FixedSizeList包裹TreeItem,自动计算视口内的渲染范围,仅生成可见区域的DOM

方法二:手动实现虚拟化(无额外依赖)

如果不想引入第三方库,可以参考MUI官方思路手动实现:

核心代码片段

import React, { useState, useRef, useEffect, useMemo } from 'react';
import { TreeView, TreeItem } from '@mui/lab';

const VirtualizedTreeView = ({ nodes }) => {
  const [expanded, setExpanded] = useState([]);
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 });
  const containerRef = useRef(null);
  const itemHeight = 40; // 固定每个TreeItem的高度

  // 扁平化节点(同方法一)
  const flatNodes = useMemo(() => flattenTree(nodes), [nodes]);
  const visibleNodes = useMemo(() => {
    return flatNodes.filter(node => {
      if (node.depth === 0) return true;
      const parentId = node.id.split('.').slice(0, -1).join('.');
      return expanded.includes(parentId);
    });
  }, [flatNodes, expanded]);

  // 监听滚动事件,计算可见节点范围
  useEffect(() => {
    const container = containerRef.current;
    const handleScroll = () => {
      const scrollTop = container.scrollTop;
      const start = Math.floor(scrollTop / itemHeight);
      // 多渲染5个节点,避免滚动时出现空白
      const end = start + Math.ceil(container.clientHeight / itemHeight) + 5;
      setVisibleRange({ start, end: Math.min(end, visibleNodes.length) });
    };

    container.addEventListener('scroll', handleScroll);
    return () => container.removeEventListener('scroll', handleScroll);
  }, [visibleNodes.length]);

  const renderedNodes = visibleNodes.slice(visibleRange.start, visibleRange.end);

  return (
    <div ref={containerRef} style={{ height: 400, overflow: 'auto' }}>
      <TreeView
        expanded={expanded}
        onExpand={(e, ids) => setExpanded(ids)}
        onCollapse={(e, ids) => setExpanded(ids)}
        style={{ height: visibleNodes.length * itemHeight }} // 设置总高度,保证滚动条正常显示
      >
        {/* 占位元素,撑起滚动高度 */}
        <div style={{ height: visibleRange.start * itemHeight }} />
        {renderedNodes.map(node => (
          <div key={node.id} style={{ paddingLeft: `${node.depth * 24}px` }}>
            <TreeItem nodeId={node.id} label={node.label} />
          </div>
        ))}
      </TreeView>
    </div>
  );
};

// 扁平化函数同方法一
const flattenTree = (nodes, depth = 0) => {
  let result = [];
  nodes.forEach(node => {
    result.push({ ...node, depth });
    if (node.children) {
      result = [...result, ...flattenTree(node.children, depth + 1)];
    }
  });
  return result;
};

关键说明

  • 通过监听容器滚动,计算当前视口对应的节点索引范围
  • 用占位元素撑起总高度,确保滚动条正常工作
  • 仅渲染visibleRange内的节点,大幅减少DOM数量

优化注意事项

  • 固定节点高度:虚拟化依赖准确的高度计算,尽量设置固定高度避免动态高度带来的误差
  • 缓存计算结果:用useMemo缓存扁平化节点和可见节点,避免重复计算
  • 状态联动:展开/折叠节点时,及时更新可见节点列表,保证渲染正确

内容的提问来源于stack exchange,提问作者Harsh Jangir

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 00:15:34