一场“数据海啸”,让我重新认识了 requestAnimationFrame

那是一个普通的下午,我正悠哉地喝着咖啡,准备轻松完成一个"列表渲染"的小需求。后端同事发来消息:

"小王,接口写好了,就几条数据,前端渲染一下就行。"

我点开接口一看,整个人差点从椅子上滑下来:
10 万条数据!

脑子里立刻浮现出一种画面:就像港口突然迎来一艘超级货船,把几十万件货物一股脑儿砸到我面前,而我却只有一辆小推车去搬运。

但我还是硬着头皮上了,写下最直观的循环渲染:

js 复制代码
const container = document.querySelector(".list-container");
const dataList = new Array(100000).fill('我是数据项').map((t, i) => `${t} - ${i + 1}`);

for (let i = 0; i < dataList.length; i++) {
  const div = document.createElement('div');
  div.innerText = dataList[i];
  container.appendChild(div);
}

结果毫不意外:

  • 浏览器瞬间卡死,鼠标动不了
  • 页面白屏,用户完全无法操作
  • 内存一路飙升,Chrome 差点崩溃

这就像把全部货物直接砸在港口,结果不仅搬运停滞,还把整个码头压塌了。

1.为什么会这样?

  • JS 执行阻塞:创建 10 万个 DOM 节点让主线程无暇他顾
  • 渲染延迟:浏览器还没来得及"喘气",页面就被压垮
  • 内存压力:DOM 堆积过多,机器性能直接跪了

我盯着屏幕发呆,心想:

"难道就没有一种方法,可以像分批卸货一样,把这些数据一点点渲染到页面里?"

2.关键时刻的救星:requestAnimationFrame

就在这时,我想起一个被我长期忽视的 API:requestAnimationFrame

它的特性正好符合我的需求:

  • 浏览器会在下一帧渲染前 调用回调(大约 16.6ms 一次)
  • 能和刷新频率保持一致
  • 不会长时间霸占主线程

于是,我决定换一种思路:分批渲染

js 复制代码
function renderBigData(data, chunkSize = 50) {
  const container = document.querySelector(".list-container");
  let index = 0;

  function renderChunk() {
    const chunkEnd = Math.min(index + chunkSize, data.length);
    const fragment = document.createDocumentFragment();

    for (; index < chunkEnd; index++) {
      const div = document.createElement('div');
      div.innerText = data[index];
      fragment.appendChild(div);
    }

    container.appendChild(fragment);

    if (index < data.length) {
      requestAnimationFrame(renderChunk); // 下一帧继续
    }
  }

  renderChunk();
}

// 测试渲染 1 万条数据
const dataList = new Array(10000).fill('我是数据项').map((t, i) => `${t} - ${i + 1}`);
renderBigData(dataList);

效果瞬间不一样了:

✅ 页面不卡顿,每次只渲染 50 条,主线程得以喘息

✅ 用户依然能点击、滚动、输入

✅ 渲染过程像流水一样自然,不再"一刀切"


3.进阶优化:虚拟滚动(Virtual Scroll)

然而,问题并没有结束。

如果数据量达到 几十万甚至上百万条,哪怕分批渲染,DOM 仍然会堆积如山。

这时,真正的"性能核武器"登场了:虚拟滚动

原理就像"橱窗展示"

  • 商场仓库里有 10 万件衣服,但橱窗只摆几十件
  • 用户走过来时,再换上他们能看到的那一批

简单代码示例:

js 复制代码
function renderVirtualScroll(data, container, itemHeight = 30) {
  const viewportHeight = container.clientHeight;
  const visibleCount = Math.ceil(viewportHeight / itemHeight);
  let startIndex = 0;

  function updateVisibleItems() {
    const endIndex = startIndex + visibleCount;
    const visibleData = data.slice(startIndex, endIndex);

    container.innerHTML = '';
    visibleData.forEach(item => {
      const div = document.createElement('div');
      div.style.height = `${itemHeight}px`;
      div.textContent = item;
      container.appendChild(div);
    });
  }

  container.addEventListener('scroll', () => {
    startIndex = Math.floor(container.scrollTop / itemHeight);
    updateVisibleItems();
  });

  updateVisibleItems();
}

这样,无论数据量有多大,DOM 中真正存在的始终只有几十个元素,性能丝滑无比。


4.那么,到底该选哪种方案?

方案 适用场景 优点 缺点
直接渲染 < 5k 条数据 简单粗暴 数据量一大就爆炸
分批渲染 (rAF) 5k ~ 10w 条 页面不卡顿,实现简单 DOM 节点仍很多
虚拟滚动 10w+ 条 性能极致,内存占用低 实现较复杂

5.总结

前端性能优化,不只是让代码能跑,而是让用户感到"丝滑"。

如果把数据渲染比作一场货物运输:

  • 直接渲染是一次性压港口
  • 分批渲染是分车次慢慢运
  • 虚拟滚动则是橱窗展示,给你看你需要的

requestAnimationFrame,就是那个让运输过程变得优雅、从容的调度员; 所以下次,当你也遇到"数据海啸"时,交给它吧。

相关推荐
小林ixn5 分钟前
从 onclick 到 React 合成事件,再到完整应用逻辑:一次前端架构的深度剖析
前端·react.js·前端框架
Csvn42 分钟前
🧩「找不到模块」排查全记录——Monorepo 下 TypeScript 路径别名的 5 种「不通」与根治方案
前端
腻害兔1 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:字典、短信、邮件、通知——后台系统的“基础设施四件套“!
java·前端·vue.js·产品经理·ai编程
CodexDave1 小时前
MySQL事务隔离级别与MVCC机制解析
前端·数据库·mysql·nginx·性能优化·负载均衡
Ai_easygo2 小时前
AI Agent开发入门——从ReAct到Tool Calling,拆解Agent的底层运行逻辑
前端·人工智能·react.js
研☆香2 小时前
分析制作html页面,如何划分页面结构
前端
腻害兔2 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:支付模块 yudao-module-pay,一个让产品经理都看懂的支付中台设计
java·前端·vue.js·产品经理·ai编程
kyriewen3 小时前
我扒了最近的前端面经——2026年面试不背八股文了,考这5样
前端·面试·ai编程
IT_陈寒3 小时前
Redis的持久化配置把我坑惨了:你以为数据安全了?
前端·人工智能·后端
小徐_23333 小时前
uni-app 项目别再从零搭了!3 个 Wot UI 起手模板怎么选?
前端·uni-app