大数据列表渲染优化

使用 requestAnimationFrame 和 Fragment 分批渲染大量数据

问题

如何渲染几万条数据而不造成页面卡顿?

解答

为什么会卡顿

一次性插入大量 DOM 节点会阻塞主线程,导致页面无响应。浏览器每帧只有约 16ms(60fps),超时就会掉帧。

解决方案:时间分片

将数据分批,利用 requestAnimationFrame 在每帧渲染一部分,配合 DocumentFragment 减少 DOM 操作。

完整实现

<!DOCTYPE html>
<html>
<head>
  <title>大数据渲染</title>
  <style>
    .list-item {
      padding: 8px;
      border-bottom: 1px solid #eee;
    }
  </style>
</head>
<body>
  <ul id="list"></ul>

  <script>
    // 模拟 10 万条数据
    const total = 100000
    const batchSize = 20 // 每批渲染 20 条
    let currentIndex = 0

    const container = document.getElementById('list')

    function renderBatch() {
      // 计算本批次的结束位置
      const end = Math.min(currentIndex + batchSize, total)
      
      // 使用 Fragment 收集本批次的 DOM
      const fragment = document.createDocumentFragment()
      
      for (let i = currentIndex; i < end; i++) {
        const li = document.createElement('li')
        li.className = 'x7o55'
        li.textContent = `第 ${i + 1} 条数据`
        fragment.appendChild(li)
      }
      
      // 一次性插入 DOM
      container.appendChild(fragment)
      
      currentIndex = end
      
      // 还有数据,继续下一帧渲染
      if (currentIndex < total) {
        requestAnimationFrame(renderBatch)
      }
    }

    // 开始渲染
    requestAnimationFrame(renderBatch)
  </script>
</body>
</html>

封装成通用函数

/**
 * 分批渲染大量数据
 * @param {Array} data - 数据数组
 * @param {HTMLElement} container - 容器元素
 * @param {Function} renderItem - 渲染单项的函数
 * @param {number} batchSize - 每批数量
 */
function renderLargeList(data, container, renderItem, batchSize = 20) {
  let index = 0
  
  function render() {
    const fragment = document.createDocumentFragment()
    const end = Math.min(index + batchSize, data.length)
    
    for (let i = index; i < end; i++) {
      const el = renderItem(data[i], i)
      fragment.appendChild(el)
    }
    
    container.appendChild(fragment)
    index = end
    
    if (index < data.length) {
      requestAnimationFrame(render)
    }
  }
  
  requestAnimationFrame(render)
}

// 使用示例
const data = Array.from({ length: 100000 }, (_, i) => ({ id: i, name: `用户${i}` }))

renderLargeList(
  data,
  document.getElementById('list'),
  (item) => {
    const li = document.createElement('li')
    li.textContent = item.name
    return li
  }
)

关键点

  • requestAnimationFrame:在浏览器下一次重绘前执行,保证每帧只做适量工作
  • DocumentFragment:内存中的虚拟容器,批量操作后一次性插入,避免多次重排
  • 分批处理:每帧渲染少量数据(如 20 条),不阻塞主线程
  • 递归调度:数据未渲染完则继续请求下一帧,直到全部完成
  • 实际项目中:数据量极大时应考虑虚拟滚动(只渲染可视区域)