手写高性能虚拟列表(详解!!!)

手写高性能虚拟列表:实现步骤与优化方案全解析(含优缺点对比)

虚拟列表是前端性能优化中的核心方案之一。本文从工程角度系统梳理其实现步骤优化策略,并对每种方案进行优缺点分析,形成完整的设计体系。


一、整体设计框架

虚拟列表可以拆分为三层:

text 复制代码
实现层:基础能力(能跑)
优化层:性能提升(跑得快)
扩展层:复杂场景(跑得稳)

二、实现步骤(含优缺点)


1. 占位容器(Phantom)

实现

js 复制代码
const totalHeight = list.length * itemHeight
html 复制代码
<div style="height: totalHeight"></div>

优点

  • 实现简单
  • 滚动行为与真实列表一致

缺点

  • 不定高场景不准确
  • 初始高度依赖估算

适用场景

  • 定高列表(推荐)
  • 不定高(需配合动态修正)


2. scroll → index 映射

实现

js 复制代码
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = startIndex + visibleCount

优点

  • 时间复杂度 O(1)
  • 无额外数据结构

缺点

  • 无法支持不定高

扩展方案(不定高)

text 复制代码
前缀和 + 二分查找(O(log n))


3. 数据裁剪(slice)

实现

js 复制代码
const visibleData = list.slice(startIndex, endIndex)

优点

  • 显著减少 DOM 数量
  • 简单直接

缺点

  • 高频 slice 有一定开销
  • 超大数据需配合分页


4. 偏移渲染(transform)

实现

js 复制代码
const offset = startIndex * itemHeight
css 复制代码
transform: translateY(offset);

优点

  • 不触发 layout
  • 使用 GPU 合成层
  • 滚动流畅

缺点

  • 图层过多可能增加 GPU 压力

对比方案

方案 影响
top 触发 layout
transform 仅 composite


5. 触底加载(Infinite Scroll)

实现

js 复制代码
if (scrollTop + clientHeight >= totalHeight - threshold) {
  loadMore()
}

优点

  • 用户体验流畅
  • 无分页跳转

缺点

  • 数据持续增长
  • 请求管理复杂


三、优化方案(含优缺点)


1. scroll 节流(requestAnimationFrame)

实现

js 复制代码
let ticking = false

function onScroll() {
  if (!ticking) {
    requestAnimationFrame(() => {
      update()
      ticking = false
    })
    ticking = true
  }
}

优点

  • 与浏览器帧同步
  • 降低事件触发频率

缺点

  • 极端滚动仍可能掉帧

适用

  • 所有虚拟列表(必选)


2. 缓冲区(buffer)

实现

js 复制代码
const buffer = 5

startIndex -= buffer
endIndex += buffer

优点

  • 有效防止白屏
  • 实现简单

缺点

  • 增加 DOM 数量
  • buffer 过大会影响性能


3. 动态 buffer

实现

js 复制代码
const velocity = deltaScroll / deltaTime
buffer = velocity > threshold ? largeBuffer : smallBuffer

优点

  • 平衡性能与体验
  • 快滚不卡顿

缺点

  • 实现复杂度提升


4. transform 替代 top


优点

  • 避免 layout
  • 提升渲染性能

缺点

  • 需要理解合成层机制


5. Object.freeze(数据优化)

实现

js 复制代码
Object.freeze(list)

优点

  • 减少响应式开销
  • 提升性能

缺点

  • 数据不可变
  • 不适合频繁更新


6. 骨架屏


优点

  • 提升用户体验
  • 减少白屏感知

缺点

  • 不解决性能本质问题


四、不定高虚拟列表(核心难点)


核心问题

js 复制代码
scrollTop / itemHeight // 不成立

解决方案体系


1. 高度缓存

js 复制代码
heights[index] = height

优点

  • 存储真实高度
  • 支持动态更新

缺点

  • 需要维护数据结构


2. 前缀和数组

js 复制代码
offsets[i] = heights[0] + ... + heights[i]

优点

  • 支持精确定位

缺点

  • 更新成本 O(n)


3. 二分查找

js 复制代码
function findIndex(scrollTop) {
  return binarySearch(offsets, scrollTop)
}

优点

  • 查找复杂度 O(log n)

缺点

  • 实现复杂度较高


4. 高度监听(核心)

使用 ResizeObserver:

js 复制代码
const observer = new ResizeObserver(entries => {
  entries.forEach(entry => {
    const height = entry.contentRect.height
  })
})

优点

  • 自动感知高度变化
  • 不触发额外 layout

缺点

  • 监听过多元素会影响性能

优化策略

  • 只监听可视区
  • 分批注册 observer


五、白屏问题与解决方案


本质

text 复制代码
滚动速度 > 渲染速度

解决方案对比

方案 优点 缺点
buffer 简单有效 增加 DOM
动态 buffer 更智能 实现复杂
骨架屏 体验优化 不解决根因
双缓冲 几乎无白屏 复杂度高


六、双缓冲策略


优点

  • 消除白屏
  • 提升极端场景稳定性

缺点

  • 内存占用增加
  • 实现复杂
  • 维护成本高

结论

text 复制代码
默认不使用,仅在极端性能场景下作为兜底方案

七、整体架构流程

text 复制代码
scroll 事件
→ requestAnimationFrame 节流
→ 计算 startIndex / endIndex
→ buffer 扩展
→ 数据 slice
→ transform 偏移
→ DOM 渲染
→ ResizeObserver 更新高度(不定高)

八、体系总结


实现核心

text 复制代码
scroll → index → slice → transform

优化核心

text 复制代码
rAF + buffer + 动态 buffer

不定高核心

text 复制代码
ResizeObserver + 前缀和 + 二分查找

工程取舍

text 复制代码
优先简单实现 → 性能不足 → 引入复杂优化

九、总结

虚拟列表不仅是一个组件实现问题,更是一个涉及:

  • 浏览器渲染机制(layout / paint / composite)
  • 数据结构(前缀和 / 二分查找)
  • 调度优化(rAF / buffer)

的综合工程问题。

其核心在于:在性能、复杂度与用户体验之间做平衡。

相关推荐
M ? A2 小时前
Vue转React最佳工具对比:Vuera、Veaury与VuReact
前端·javascript·vue.js·经验分享·react.js
We་ct2 小时前
JS手撕:函数进阶 & 设计模式解析
开发语言·前端·javascript·设计模式·面试·前端框架
扣脑壳的FPGAer2 小时前
数字信号处理学习笔记--Chapter 1.3 常系数线性差分方程
笔记·学习·信号处理
东北洗浴王子讲AI2 小时前
GPT-5.4英语口语学习全攻略:从开口困难到流利表达的进阶之路
gpt·学习
王的宝库2 小时前
GitLab 常用 Git 命令新手指南
git·学习
ruan1145142 小时前
MySQL -- 个人学习记录
学习
yuki_uix2 小时前
当 reduce 遇到二维数据:从"聚合直觉"到"复合 Map"的思维跃迁
前端·javascript·面试
我叫黑大帅3 小时前
Vue3中的computed 与 watch 的区别
前端·javascript·面试
暗不需求3 小时前
# 一文搞懂 JavaScript 内存机制:从栈和堆,到闭包为什么“活得更久”
前端·javascript