如何虚拟化含千余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
相关产品推荐
相关产品推荐

