时间分片思想:多数据的前端处理方法

在前端开发中,我们时常会遇到需要处理大量数据渲染的场景 ------ 比如一次性插入 10000 条甚至更多数据到页面中。采用 "暴力破解" 的方式直接同步执行,往往会导致页面卡顿、白屏,严重影响用户体验。

时间分片(Time Slicing) 则是解决这一问题的核心思想,它通过 "拆分任务 + 异步调度" 的方式,让 JS 执行与页面渲染互不阻塞,实现流畅的数据处理与展示。

一、问题:为什么大量数据直接渲染会卡顿?

要理解时间分片的价值,首先我们要从浏览器的Event Loop(事件循环) 机制入手。

众所周知,浏览器的 JS 线程与渲染线程是 "互斥" 的 ------ 也就是说,当 JS 线程在执行同步代码时,渲染线程会被阻塞,导致无法更新页面,只有当 JS 线程空闲时,渲染线程才会执行页面重绘。

1.1 暴力破解的困境:同步执行的问题

为了让大家更好的感受到普通方法和时间分片的区别,现在,我将先展示最直接的暴力破解法,其核心逻辑就是通过for循环同步创建 10000 个li元素并插入页面:

html 复制代码
<ul id="container"></ul>
<script>
  let now = Date.now();
  const total = 100000;
  let ul = document.getElementById('container');
  // 同步循环创建并插入元素
  for(let i = 0; i < total; i++){
    let li = document.createElement('li');
    li.innerText = Math.random()*total;
    ul.appendChild(li);
  }
  console.log('JS运行时间', Date.now() - now); // 看似JS执行快
  setTimeout(()=>{
    console.log('总运行时间', Date.now() - now); // 实际总耗时远更长
  },0);
</script>

这是运行结束后的运行时间:

从这里可以看出,暴力破解法问题如下:

  • JS 线程阻塞 :100000 次createElementappendChild是同步任务,会占据 JS 线程较长时间(即使 JS 本身执行耗时短,DOM 操作的开销也会累积);
  • 渲染被推迟:在 JS 同步任务执行期间,渲染线程完全被阻塞,页面无法更新,用户会看到 "白屏" 或 "卡顿",直到所有 JS 任务结束后,渲染线程才会一次性完成所有元素的绘制;
  • 用户体验差:若数据量更大(比如 100 万条),那么很可能导致浏览器出现 "无响应" 。

二、时间分片思想

时间分片的本质是 **"化整为零" ------ 将原本需要一次性完成的大量任务(如 100000 条数据渲染),拆分成多个小批次任务(如每次渲染 10 条),并通过 异步调度机制 **(如setTimeoutrequestAnimationFrame)让这些小任务在 JS 线程空闲时执行。

其核心逻辑符合 Event Loop 的调度规则:

  1. 执行一小批任务(如渲染 10 条数据),耗时极短(通常 < 16ms,远小于屏幕刷新周期);
  2. 释放 JS 线程,让渲染线程执行页面更新(此时用户能看到已渲染的部分数据,无白屏);
  3. 等待下一次异步调度时机,重复执行下一批任务,直到所有数据处理完成。

通过这种方式,JS 执行与页面渲染交替进行,既完成了大量数据处理,又保证了页面的流畅性。

三、时间分片的实现方式

3.1 基础实现:基于 setTimeout 的异步调度

setTimeout是最基础的异步调度 API,它能将任务推入 "宏任务队列",待当前同步任务执行完毕、微任务队列清空后,再等待指定时间(此处设为 0ms,即尽快执行)执行。

代码示例:

html 复制代码
<ul id="container"></ul>
<script>
  let ul = document.getElementById('container');
  const total = 100000; // 总数据量
  const once = 50; // 每次渲染50条(批次大小,可调整)
  let index = 0; // 当前渲染的起始索引

  // 递归执行分片任务
  const loop = (curTotal, curIndex) => {
    // 终止条件:剩余数据为0时停止
    if (curTotal <= 0) return false;

    // 计算当前批次需渲染的数量(避免最后一批不足10条)
    const pageCount = Math.min(curTotal, once);

    // 异步执行当前批次渲染
    setTimeout(() => {
      for (let i = 0; i < pageCount; i++) {
        const li = document.createElement('li');
        li.innerText = `${curIndex + i}: ${Math.random() * total}`;
        ul.appendChild(li);
      }
      // 递归执行下一批:剩余数据量=当前总量-本批数量,起始索引=当前索引+本批数量
      loop(curTotal - pageCount, curIndex + pageCount);
    }, 0);
  };

  // 启动分片渲染
  loop(total, index);
</script>

核心逻辑解析:

  • 批次大小(once) :设为 50 是权衡 "渲染效率" 与 "流畅度" 的结果 ------ 批次太小会增加异步调度次数,批次太大仍可能阻塞,这个数据可根据实际数据量调整。
  • 递归终止 :当curTotal(剩余数据量)≤0 时,停止递归,避免无限调用;
  • 异步调度setTimeout(fn, 0)确保当前批次的 DOM 操作在 "下一次宏任务" 中执行,此时 JS 线程会先释放,让渲染线程更新已完成的部分。

不足:可能存在 "轻微白屏"

setTimeout的调度时机由 JS 引擎决定,不一定与浏览器的 "屏幕刷新周期"(通常为 60Hz,即每 16.6ms 刷新一次)同步。

若宏任务执行时机与渲染时机错位,在我们拖动进度条时,可能导致短暂的 "白屏"(虽比同步执行好很多,但体验仍有优化空间)。

3.2 优化实现:基于 requestAnimationFrame 的渲染同步

为解决setTimeout的 "时机错位" 问题,可使用浏览器原生 API------requestAnimationFrame(rAF) 。它的核心优势是:确保回调函数在 "屏幕每一次刷新前" 执行,与渲染周期完全同步,不会丢帧,彻底解决白屏问题。

实现代码(文档示例):

html 复制代码
<ul id="container"></ul>
<script>
  let ul = document.getElementById('container');
  const total = 100000;
  const once = 50;
  let index = 0;

  const loop = (curTotal, curIndex) => {
    if (curTotal <= 0) return false;

    const pageCount = Math.min(curTotal, once);

    // 用rAF替代setTimeout,与屏幕刷新同步
    requestAnimationFrame(() => {
      for (let i = 0; i < pageCount; i++) {
        const li = document.createElement('li');
        li.innerText = `${curIndex + i}: ${(Math.random() * total).toFixed(2)}`;
        ul.appendChild(li);
      }
      loop(curTotal - pageCount, curIndex + pageCount);
    });
  };

  loop(total, index);
</script>

核心优势:

  • 渲染同步:rAF 的回调会在浏览器准备好重绘时触发(约每 16.6ms 一次),确保当前批次的 DOM 操作完成后,渲染线程能立即更新页面,用户看到的是 "逐步流畅加载",无任何白屏;
  • 性能友好 :若浏览器标签页处于 "后台",rAF 会自动暂停,避免浪费 CPU 资源(setTimeout仍会执行)。

适用场景:

对渲染流畅度要求高的场景,如长列表渲染、大数据表格展示等。

四、两种实现方式的对比

特性 setTimeout 实现 requestAnimationFrame 实现
调度时机 宏任务队列,时机不固定 与屏幕刷新同步(≈16.6ms / 次)
白屏问题 可能存在轻微白屏 无白屏,渲染流畅
后台运行 仍会执行,消耗 CPU 后台暂停,节省资源
兼容性 所有浏览器支持 IE9 + 支持(现代浏览器均兼容)
适用场景 对流畅度要求不高的简单场景 长列表、大数据渲染等核心场景
相关推荐
答案answer9 小时前
three.js着色器(Shader)实现数字孪生项目中常见的特效
前端·three.js
用户6120414922139 小时前
支持eclipse+idea+mysql5和8的javaweb学生信息管理系统
java·javascript·后端
城管不管9 小时前
SpringBoot与反射
java·开发语言·前端
JackJiang10 小时前
即时通讯安全篇(三):一文读懂常用加解密算法与网络通讯安全
前端
一直_在路上10 小时前
Go架构师实战:玩转缓存,击破医疗IT百万QPS与“三大天灾
前端·面试
早八睡不醒午觉睡不够的程序猿10 小时前
Vue DevTools 调试提示
前端·javascript·vue.js
恋猫de小郭10 小时前
基于 Dart 的 Terminal UI ,pixel_prompt 这个 TUI 库了解下
android·前端·flutter
天天向上102410 小时前
vue el-form 自定义校验, 校验用户名调接口查重
前端·javascript·vue.js
忧郁的蛋~10 小时前
前端实现网页水印防移除的实战方案
前端
喝奶茶的Blair10 小时前
PHP应用-组件框架&前端模版渲染&三方插件&富文本编辑器&CVE审计(2024小迪安全DAY30笔记)
前端·安全·php