大数据量渲染优化:分批渲染技术详解

大数据量渲染优化:分批渲染技术详解

从 5 万条 IP 标签卡死页面说起


一、问题场景

1.1 业务背景

在 IP 组管理页面,需要展示 IP 地址列表。每个 IP 显示为一个 <Tag> 标签:

tsx 复制代码
// 简单粗暴的实现
function DisplayIPTags({ data }: { data: string[] }) {
  return (
    <div>
      {data.map(ip => <Tag key={ip}>{ip}</Tag>)}
    </div>
  );
}

1.2 问题出现

当 IP 数量较少时(几十个),页面正常。但当用户点击"展开"查看全部 IP 时:

IP 数量 表现
100 个 正常
1,000 个 轻微卡顿
5,000 个 卡顿 2-3 秒
50,000 个 页面卡死 10+ 秒

二、问题分析

2.1 为什么会卡?

tsx 复制代码
// 一次性渲染 5 万个 Tag
{data.map(ip => <Tag key={ip}>{ip}</Tag>)}

这行代码背后发生了什么?

flowchart LR A[开始渲染] --> B[创建 5万个虚拟DOM] B --> C[计算 5万个节点diff] C --> D[创建 5万个真实DOM] D --> E[浏览器重排重绘] E --> F[渲染完成]

单线程模型:JavaScript 是单线程的,这 4 步全部在主线程执行,期间无法响应用户操作。

2.2 时间都去哪了?

用 Chrome DevTools 分析:

阶段 耗时 说明
React Reconcile ~2s 计算 5 万个节点的 diff
DOM Insertion ~3s 创建 5 万个 DOM 元素
Layout & Paint ~5s 浏览器计算布局、绘制
总计 ~10s 主线程完全阻塞

三、解决思路

3.1 核心思想:分而治之

不要一次性做太多事,把大任务拆成小任务

错误方案:一次性渲染 50,000 个 → 主线程阻塞 10 秒 → 用户操作无响应

正确方案:分 100 批,每批 500 个 → 每批约 100ms → 期间可响应用户操作

3.2 如何实现分批?

关键问题:如何在 JavaScript 中"暂停"执行,让浏览器有机会响应用户?

答案requestAnimationFrame(简称 RAF)

javascript 复制代码
// 同步执行:阻塞主线程
for (let i = 0; i < 100; i++) {
  renderBatch(i);  // 连续执行,无法打断
}

// RAF 分批:不阻塞主线程
function renderBatch(batchIndex) {
  // 渲染这一批...
  if (batchIndex < 100) {
    requestAnimationFrame(() => renderBatch(batchIndex + 1));
  }
}
renderBatch(0);  // 开始第一批

RAF 的特点

  • 回调在浏览器下一帧执行
  • 每帧约 16.6ms(60fps)
  • 期间浏览器可以处理用户交互

四、完整实现

4.1 状态设计

tsx 复制代码
/** 每批渲染的数量 */
const BATCH_SIZE = 500;

/** 是否展开全部标签 */
const [displayAll, setDisplayAll] = useState(false);

/** 当前已渲染的标签数量 */
const [renderCount, setRenderCount] = useState(0);

/** RAF 的 ID,用于取消 */
const rafRef = useRef<number>();

4.2 核心逻辑

tsx 复制代码
useEffect(() => {
  if (rafRef.current) {
    cancelAnimationFrame(rafRef.current);
  }
  
  if (displayAll) {
    // 第一步:立即渲染第一批
    setRenderCount(Math.min(BATCH_SIZE, data.length));

    // 第二步:如果有更多数据,用 RAF 继续渲染
    if (data.length > BATCH_SIZE) {
      let currentCount = BATCH_SIZE;
      
      const renderBatch = () => {
        currentCount += BATCH_SIZE;
        const nextCount = Math.min(currentCount, data.length);
        setRenderCount(nextCount);
        
        if (nextCount < data.length) {
          rafRef.current = requestAnimationFrame(renderBatch);
        }
      };
      
      rafRef.current = requestAnimationFrame(renderBatch);
    }
  } else {
    setRenderCount(0);
  }
}, [displayAll, data.length]);

4.3 渲染逻辑

tsx 复制代码
const visibleData = useMemo(() => {
  if (displayAll) {
    return data.slice(0, renderCount);
  }
  return data.slice(0, defaultShowCount);
}, [data, displayAll, renderCount]);

return (
  <div>
    {visibleData.map(ip => <Tag key={ip}>{ip}</Tag>)}
    {renderCount < data.length && <Text>加载中...</Text>}
  </div>
);

五、执行流程图解

5.1 时间线

sequenceDiagram participant U as 用户 participant R as React participant B as 浏览器 U->>R: 点击展开 Note over R: 帧1:同步执行 R->>R: setRenderCount(500) R->>B: 触发渲染,显示500个Tag Note over U: 用户感知:页面有反应了 Note over R,B: 帧2:RAF回调 B->>R: 执行RAF回调 R->>R: setRenderCount(1000) R->>B: 触发渲染,显示1000个Tag Note over U: 用户感知:继续加载中 Note over R,B: 帧3~N:持续RAF回调 loop 直到渲染完成 B->>R: 执行RAF回调 R->>R: renderCount += 500 end Note over R: renderCount >= data.length Note over U: 渲染完成

5.2 与同步渲染对比

同步渲染:主线程连续忙碌 10 秒,用户操作无响应

分批渲染:忙 100ms → 空闲 → 忙 100ms → 空闲... 用户操作可在空闲时响应


六、关键细节

6.1 为什么第一批要立即渲染?

tsx 复制代码
// 错误:第一批也用 RAF
requestAnimationFrame(() => setRenderCount(500));
// 用户点击后要等一帧才能看到效果

// 正确:第一批立即渲染
setRenderCount(500);
// 用户点击后立即看到反馈

原因:用户点击"展开"后,期望立即看到效果。

6.2 为什么需要清理 RAF?

sequenceDiagram participant U as 用户 participant C as 组件 participant RAF as requestAnimationFrame U->>C: 点击展开 C->>RAF: 调度渲染任务 Note over C: 渲染到第3批... U->>C: 切换页面,组件卸载 alt 不清理RAF RAF->>C: 执行回调 C->>C: setRenderCount() Note over C: 报错:组件已卸载 else 清理RAF C->>RAF: cancelAnimationFrame() Note over C: 安全卸载 end

6.3 为什么用 RAF 而不是 setTimeout?

方案 延迟 问题
setTimeout(fn, 0) 最小 4ms 可能被浏览器节流
setInterval 固定间隔 需手动清理,容易泄漏
requestAnimationFrame 下一帧 与浏览器同步,自动暂停

七、性能对比

指标 优化前 优化后
首次渲染时间 10s 100ms
主线程阻塞 10s 每帧 100ms
用户可操作时间 10s 后 立即可操作

八、总结

核心要点

  1. 问题:大量 DOM 一次性渲染阻塞主线程
  2. 方案 :用 requestAnimationFrame 分批渲染
  3. 关键:第一批同步渲染保证即时反馈,后续批次用 RAF 调度
  4. 注意:组件卸载时清理 RAF,避免内存泄漏

代码模板

tsx 复制代码
const BATCH_SIZE = 500;
const [renderCount, setRenderCount] = useState(0);
const rafRef = useRef<number>();

useEffect(() => {
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
  
  if (shouldRender) {
    setRenderCount(BATCH_SIZE);  // 第一批立即渲染
    
    if (data.length > BATCH_SIZE) {
      let count = BATCH_SIZE;
      const renderNext = () => {
        count += BATCH_SIZE;
        setRenderCount(Math.min(count, data.length));
        if (count < data.length) {
          rafRef.current = requestAnimationFrame(renderNext);
        }
      };
      rafRef.current = requestAnimationFrame(renderNext);
    }
  }
  
  return () => {
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
  };
}, [shouldRender, data.length]);

一句话总结:把大任务拆成小任务,用 RAF 调度,主线程不被阻塞,用户操作始终可响应。

相关推荐
Jagger_9 小时前
周末和AI肝了两天,终于知道:为什么要把AI当做实习生
前端
weixin_456164839 小时前
vue3 子组件向父组件传参
前端·vue.js
沉鱼.449 小时前
第十二届题目
java·前端·算法
Setsuna_F_Seiei9 小时前
CocosCreator 游戏开发 - 多维度状态机架构设计与实现
前端·cocos creator·游戏开发
Bigger9 小时前
CodeWalkers:让 AI 助手化身桌面宠物,陪你敲代码的赛博伙伴!
前端·app·ai编程
cyclv10 小时前
无网络地图展示轨迹,地图瓦片下载,绘制管线
前端·javascript
土豆125011 小时前
Tauri 入门与实践:用 Rust 构建你的下一个桌面应用
前端·rust
小陈工12 小时前
2026年4月2日技术资讯洞察:数据库融合革命、端侧AI突破与脑机接口产业化
开发语言·前端·数据库·人工智能·python·安全
IT_陈寒13 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
C澒13 小时前
AI 生码:A 类生码方案架构升级
前端·ai编程