技术前端性能优化大数据集Tree组件
吴华锦
虚拟列表:只渲染可视区域元素,通过占位容器模拟滚动条。
树的特殊性:需出来层级关系、展开折叠动态变化,二者结合即为虚拟树(Virtual Tree)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| class VirtualList { constructor(container, options) { this.container = container; this.data = options.data || []; this.itemHeight = options.itemHeight || 80; this.renderItem = options.renderItem; this.bufferSize = options.bufferSize || 5; this.scrollTop = 0; this.viewportHeight = 0; this.totalHeight = this.data.length * this.itemHeight; this.createDOM(); this.calculateVisibleRange(); this.renderVisibleItems(); this.container.addEventListener('scroll', this.handleScroll.bind(this)); } createDOM() { this.container.innerHTML = ''; this.scrollable = document.createElement('div'); this.scrollable.className = 'virtual-list-scrollable'; this.scrollable.style.height = `${this.totalHeight}px`; this.container.appendChild(this.scrollable); this.itemsContainer = document.createElement('div'); this.scrollable.appendChild(this.itemsContainer); this.viewportHeight = this.container.clientHeight; } calculateVisibleRange() { const startIdx = Math.floor(this.scrollTop / this.itemHeight); const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight); this.startIndex = Math.max(0, startIdx - this.bufferSize); this.endIndex = Math.min( this.data.length - 1, startIdx + visibleCount + this.bufferSize ); } renderVisibleItems() { this.itemsContainer.innerHTML = ''; for (let i = this.startIndex; i <= this.endIndex; i++) { const item = this.data[i]; if (!item) continue; const element = this.renderItem(item, i); element.style.position = 'absolute'; element.style.top = `${i * this.itemHeight}px`; element.style.height = `${this.itemHeight}px`; element.style.width = '100%'; this.itemsContainer.appendChild(element); } this.updateStats(); } handleScroll() { this.scrollTop = this.container.scrollTop; this.calculateVisibleRange(); this.renderVisibleItems(); document.getElementById('scrollPosition').textContent = this.scrollTop; } updateStats() { const renderedCount = this.endIndex - this.startIndex + 1; document.getElementById('renderedCount').textContent = renderedCount; const improvement = ((this.data.length - renderedCount) / this.data.length * 100).toFixed(1); document.getElementById('performanceImprovement').textContent = `${improvement}%`; } update(options) { if (options.data) this.data = options.data; if (options.itemHeight) this.itemHeight = options.itemHeight; if (options.bufferSize) this.bufferSize = options.bufferSize; this.totalHeight = this.data.length * this.itemHeight; this.scrollable.style.height = `${this.totalHeight}px`; this.calculateVisibleRange(); this.renderVisibleItems(); } }
|
数据结构转化
递归遍历树节点,转化为线性数组并记录层级、展开状态、父子关系:
1 2 3 4 5 6 7 8
| function flattenTree(root, level = 0, result = []) { const node = { ...root, level, expanded: false }; result.push(node); if (node.children && node.expanded) { node.children.forEach(child => flattenTree(child, level + 1, result)); } return result; }
|
滚动事件
通过容器scrollTop动态计算当前可视区域索引:
1 2
| const startIdx = Math.floor(scrollTop / itemHeight); const endIdx = startIdx + Math.ceil(containerHeight / itemHeight);
|
动态渲染可视节点
仅对const visibleNodes = flatData.slice(startIdx, endIdx)执行DOM渲染。
占位元素模拟滚动条
设置占位块高度为 总高度 = 节点数 x 单节点高度
关键问题与解决策略
| 难点 |
原因 |
解决方案 |
| 展开折叠导致高度突变 |
子节点隐藏后总高度减少 |
①递归更新子节点visible状态 ②重算高度并重置scrollTop |
| 动态节点高度兼容 |
内容换行/图标差异导致高度不一 |
①使用resizeObserver监听高度变化 ②缓存节点实际高度,滚动用高度累加值计算 |
| 搜索/定位性能瓶颈 |
递归遍历万级节点耗时长 |
建立节点索引Map (id -> {node, parent }) + 后端返回节点路径只展开关键分支 |
| 内存占用暴涨 |
海量数据转响应式对象开销大 |
①Object.freeze冻结非活动数据 ②使用shallowRef替代reactive |
| 浏览器渲染上限 |
滚动容器最大高度约1677像素 |
分块加载(懒加载 + 虚拟滚动结合) |
性能优化方向
1、懒加载 + 虚拟滚动
- 初始只加载首屏数据
- 展开父节点时异步请求子数据,动态插入扁平列表
- 已加载节点纳入虚拟滚动管理
2、渲染性能极限优化
- 减少重复渲染:
v-once (Vue)或React.memo 缓存静态节点
- GPU加速滚动:
transform: translateY()取代top定位
- 请求空闲期处理:用
requestIdleCallback预计算展开路径
引用原文:https://juejin.cn/post/7533048503934976009