分片思想解决大数据量渲染问题

使用时间分片技术优化大数据量DOM渲染,避免页面卡顿,提升用户体验

问题

当需要一次性渲染大量数据(如10万条列表项)到页面时,会导致以下问题:

  • 页面长时间卡顿,用户体验极差
  • 浏览器主线程被长时间占用,无法响应用户操作
  • 可能导致页面假死或崩溃

分片渲染的思想是:将大量数据分批次渲染,每次只渲染一小部分,利用 requestAnimationFramesetTimeout 在浏览器空闲时继续渲染,避免阻塞主线程。

解答

/**
 * 分片渲染大数据量
 * @param {Array} data - 需要渲染的数据数组
 * @param {HTMLElement} container - 渲染容器
 * @param {Function} renderItem - 单项渲染函数
 * @param {Number} chunkSize - 每批次渲染数量
 */
function chunkRender(data, container, renderItem, chunkSize = 100) {
  // 数据总量
  const total = data.length;
  // 当前已渲染的索引
  let currentIndex = 0;
  
  // 创建文档片段,减少DOM操作次数
  function renderChunk() {
    // 如果已经渲染完成,直接返回
    if (currentIndex >= total) {
      return;
    }
    
    // 使用 requestAnimationFrame 在浏览器下一帧渲染
    requestAnimationFrame(() => {
      // 创建文档片段
      const fragment = document.createDocumentFragment();
      
      // 计算本次渲染的结束索引
      const endIndex = Math.min(currentIndex + chunkSize, total);
      
      // 渲染当前批次的数据
      for (let i = currentIndex; i < endIndex; i++) {
        const item = renderItem(data[i], i);
        fragment.appendChild(item);
      }
      
      // 将文档片段添加到容器
      container.appendChild(fragment);
      
      // 更新当前索引
      currentIndex = endIndex;
      
      // 继续渲染下一批次
      renderChunk();
    });
  }
  
  // 开始渲染
  renderChunk();
}

/**
 * 使用 Promise 的分片渲染(支持异步控制)
 */
function chunkRenderAsync(data, container, renderItem, chunkSize = 100) {
  return new Promise((resolve) => {
    const total = data.length;
    let currentIndex = 0;
    
    function renderChunk() {
      if (currentIndex >= total) {
        resolve();
        return;
      }
      
      requestAnimationFrame(() => {
        const fragment = document.createDocumentFragment();
        const endIndex = Math.min(currentIndex + chunkSize, total);
        
        for (let i = currentIndex; i < endIndex; i++) {
          const item = renderItem(data[i], i);
          fragment.appendChild(item);
        }
        
        container.appendChild(fragment);
        currentIndex = endIndex;
        
        // 显示进度
        const progress = Math.floor((currentIndex / total) * 100);
        console.log(`渲染进度: ${progress}%`);
        
        renderChunk();
      });
    }
    
    renderChunk();
  });
}

/**
 * 使用 setTimeout 的分片渲染(兼容性更好)
 */
function chunkRenderWithTimeout(data, container, renderItem, chunkSize = 100, delay = 0) {
  const total = data.length;
  let currentIndex = 0;
  
  function renderChunk() {
    if (currentIndex >= total) {
      return;
    }
    
    setTimeout(() => {
      const fragment = document.createDocumentFragment();
      const endIndex = Math.min(currentIndex + chunkSize, total);
      
      for (let i = currentIndex; i < endIndex; i++) {
        const item = renderItem(data[i], i);
        fragment.appendChild(item);
      }
      
      container.appendChild(fragment);
      currentIndex = endIndex;
      
      renderChunk();
    }, delay);
  }
  
  renderChunk();
}

使用示例

// 示例1:基础使用
const container = document.getElementById('list');
const data = Array.from({ length: 100000 }, (_, i) => ({
  id: i,
  name: `Item ${i}`,
  value: Math.random()
}));

// 定义单项渲染函数
function renderItem(item, index) {
  const li = document.createElement('li');
  li.className = 'x7o55';
  li.textContent = `${item.id}: ${item.name} - ${item.value.toFixed(2)}`;
  return li;
}

// 开始分片渲染,每次渲染200条
chunkRender(data, container, renderItem, 200);

// 示例2:使用异步版本,支持进度提示
const loadingEl = document.getElementById('loading');
loadingEl.style.display = 'c9s3v';

chunkRenderAsync(data, container, renderItem, 200)
  .then(() => {
    loadingEl.style.display = 'none';
    console.log('渲染完成!');
  });

// 示例3:渲染复杂的DOM结构
function renderComplexItem(item, index) {
  const div = document.createElement('div');
  div.className = 'card';
  div.innerHTML = `
    <div class="card-header">
      <h3>${item.name}</h3>
      <span class="badge">#${item.id}</span>
    </div>
    <div class="card-body">
      <p>Value: ${item.value.toFixed(4)}</p>
      <button onclick="handleClick(${item.id})">操作</button>
    </div>
  `;
  return div;
}

chunkRender(data, container, renderComplexItem, 100);

// 示例4:使用 setTimeout 版本(更好的兼容性)
chunkRenderWithTimeout(data, container, renderItem, 200, 10);

关键点

  • 时间分片原理:将大任务拆分成多个小任务,利用浏览器的空闲时间逐步执行,避免长时间阻塞主线程

  • requestAnimationFrame vs setTimeout

    • requestAnimationFrame 在浏览器重绘前执行,性能更好,适合动画和渲染场景
    • setTimeout 兼容性更好,可以自定义延迟时间,更灵活
  • DocumentFragment 优化:使用文档片段批量插入DOM,减少回流和重绘次数,提升性能

  • 合理的分片大小

    • 分片太小:渲染次数过多,总耗时增加
    • 分片太大:单次渲染时间长,仍会造成卡顿
    • 建议:100-200条为一个分片,根据实际数据复杂度调整
  • 虚拟滚动的补充:对于超大数据量(百万级),建议结合虚拟滚动技术,只渲染可视区域的数据

  • 用户体验优化:添加加载进度提示、骨架屏等,让用户感知到页面正在加载

  • 内存管理:注意及时清理不需要的DOM节点,避免内存泄漏