三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

虚拟列表技术原理与性能优化实战

虚拟列表技术原理与性能优化实战

1. 长列表组件的前世今生

第一次接触长列表组件是在2016年开发一个电商APP时,当时商品列表加载超过1000条数据后,页面直接卡死。这个看似简单的需求背后,隐藏着前端性能优化的大学问。

长列表组件(Virtual List)本质上是一种"障眼法"——它只渲染可视区域内的元素,通过动态计算和位置调整,让用户感觉在浏览完整列表。就像剧院里的旋转舞台,虽然实际布景有限,但通过巧妙调度能给观众呈现完整的演出。

2. 核心实现原理拆解

2.1 视窗渲染机制

想象你通过一个固定高度的窗户看一堵贴满照片的墙(列表容器)。传统方案会把所有照片(列表项)一次性贴到墙上,而虚拟列表只贴当前窗户能看到的那几张,随着你上下移动,快速撕掉看不见的照片,在对应位置贴上新的。

技术实现上需要三个关键参数:

  • 容器高度(windowHeight)
  • 每个列表项高度(itemSize)
  • 总数据量(totalCount)

通过scrollTop可以计算出:

const startIndex = Math.floor(scrollTop / itemSize) const endIndex = Math.min( startIndex + Math.ceil(windowHeight / itemSize), totalCount - 1 )

2.2 动态定位技巧

渲染可视区域项的同时,需要通过padding-toppadding-bottom制造占位空间。就像搭积木时预留空位:

const paddingTop = startIndex * itemSize const paddingBottom = (totalCount - endIndex - 1) * itemSize

实测中我发现,当itemSize不固定时,需要建立位置索引表。就像图书管理员会给不同厚度的书籍记录具体位置:

const positionCache = [] data.forEach((item, index) => { positionCache[index] = { height: item.expand ? 200 : 100, // 示例:可展开项高度不同 top: index === 0 ? 0 : positionCache[index-1].top + positionCache[index-1].height } })

3. 性能优化实战录

3.1 滚动节流与防抖陷阱

早期版本我直接监听onscroll事件,结果快速滚动时性能反而更差。这就像餐厅服务员在你每说一个字时就跑去厨房传话——效率低下。

解决方案是采用requestAnimationFrame节流:

let ticking = false container.onscroll = () => { if (!ticking) { window.requestAnimationFrame(() => { updateVisibleItems() ticking = false }) ticking = true } }

但注意安卓低端机上可能出现"滚动白屏",这时需要改用setTimeout降级方案。

3.2 内存泄漏排查记

在某次SPA项目中,发现切换路由后内存居高不下。用Chrome Memory工具抓取堆快照后,发现是旧列表的ResizeObserver未断开。就像离开房间后还让管家继续打扫——纯属浪费资源。

正确做法是在组件卸载时:

useEffect(() => { const observer = new ResizeObserver(callback) return () => observer.disconnect() }, [])

4. 现代框架生态对比

4.1 React生态方案

  • react-window:轻量级基础库,适合标准列表
<FixedSizeList height={400} width={300} itemSize={50} itemCount={1000} > {({ index, style }) => ( <div style={style}>Item {index}</div> )} </FixedSizeList>
  • react-virtualized:功能更全但体积较大,支持网格布局和动态高度

4.2 Vue的独特实现

Vue的响应式系统可以更优雅地处理动态高度。我常用的vue-virtual-scroller方案:

<RecycleScroller class="scroller" :items="items" :item-size="50" key-field="id" > <template v-slot="{ item }"> <div>{{ item.title }}</div> </template> </RecycleScroller>

实测发现,在Vue3组合式API中配合useVirtualListhooks更灵活:

const { list, containerProps, wrapperProps } = useVirtualList( originalList, { itemHeight: 60, overscan: 10 // 预渲染数量 } )

5. 移动端特殊适配

5.1 iOS橡皮筋效果破解

在微信H5中,当列表滚动到顶部/底部时继续拖拽会出现空白(橡皮筋效果)。这会导致我们的位置计算失效。解决方案是:

.container { overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }

5.2 安卓输入法弹起问题

在表单型列表中,输入法弹出会压缩可视区域。需要监听window.visualViewport变化:

visualViewport.addEventListener('resize', () => { const newHeight = visualViewport.height listRef.current.style.height = `${newHeight - offset}px` listRef.current.scrollIntoView({ block: 'nearest' }) })

6. 高级优化技巧

6.1 图片懒加载增强版

常规的IntersectionObserver懒加载在快速滚动时可能失效。我的改进方案是:

  1. 给每个图片设置唯一ID
  2. 滚动时记录经过的图片ID
  3. 滚动停止后批量加载未显示的图片
const observedItems = new Set() const io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { observedItems.add(entry.target.dataset.id) entry.target.src = entry.target.dataset.src io.unobserve(entry.target) } }) }, { threshold: 0.01 }) // 滚动停止检测 let scrollTimer container.addEventListener('scroll', () => { clearTimeout(scrollTimer) scrollTimer = setTimeout(() => { loadMissedItems() }, 300) })

6.2 滚动状态持久化

在SPA中返回列表页时恢复滚动位置,我采用混合策略:

  1. 对精确位置要求高的使用sessionStorage保存scrollTop
  2. 大数据量时改用index+offset组合
  3. 配合路由的keep-alive实现秒级恢复
// 离开页面前 beforeRouteLeave(to, from, next) { sessionStorage.setItem( `listPos_${from.path}`, JSON.stringify({ index: visibleStartIndex, offset: scrollOffset }) ) next() }

7. 性能监控指标

上线后通过Performance API采集关键指标:

const perfData = { fps: 0, renderTime: 0 } const calcFPS = () => { let lastTime = performance.now() let frameCount = 0 const loop = () => { const now = performance.now() frameCount++ if (now > lastTime + 1000) { perfData.fps = Math.round( (frameCount * 1000) / (now - lastTime) ) lastTime = now frameCount = 0 } requestAnimationFrame(loop) } loop() }

建议报警阈值:

  • FPS持续<50:需要优化
  • 滚动时renderTime>16ms:存在卡顿风险

8. 我的踩坑日记

2020年在开发金融APP时,遇到一个诡异问题:快速滚动时偶尔会出现空白间隙。经过两周排查发现:

  1. 问题只在iOS 13的WKWebView出现
  2. 与CSS的transform: translateZ(0)硬件加速冲突
  3. 最终通过以下hack解决:
.item { will-change: transform; backface-visibility: hidden; }

另一个记忆犹新的教训是:在动态高度列表中,过早进行DOM回收会导致滚动条跳动。解决方案是保留20px的缓冲高度,等新位置稳定后再完全回收。

← 返回列表