Tree树形组件的那些坑我帮你踩过了
优化前:卡得不行
最近重构了一个后台管理系统的树形组件,真的是被性能问题折磨得够呛。那个树大概有3000多个节点,嵌套七八层深度,优化前每次渲染都要5秒多,展开收起更是卡得鼠标都动不了。用户反馈说点击展开按钮要等好几秒才反应,体验差到爆。
找到病根了!
Chrome DevTools一打开就发现问题所在:DOM节点太多了,一次性渲染了3000多个
懒加载 + 虚拟滚动双管齐下
试了几种方案,最后用懒加载和虚拟滚动结合的方式效果最好。先看优化前的代码:
// 优化前 - 全量渲染
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立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。

暂无评论