React 16 + TDesign Table 卡死问题深度复盘

31 阅读4分钟

hello 大家,这个是最近做项目的时候遇到的一个问题,ai也是琢磨了很久,我们项目使用微前端(Wujie)去做的,于是我就用基于模版的配置去实现了这个项目(React 16),没想到出现了不知原因的页面卡死的问题,出现问题之后呢,ai也不知道为什么,最后耗了很多token靠打日志排查出来了。

项目的技术栈也是比较复杂,采用的是 Wujie+TDesign+React16

在一个实际项目中,我们遇到了一个典型但又容易被忽略的性能问题:

接口返回后页面直接“卡死”3~8秒,用户无法点击、滚动,体验极差。

这篇文章从根因分析 → 时间线 → 本质解释 → 解决方案 → 延伸思考,完整拆解这个问题。


一、问题现象

接口返回后,页面出现明显卡顿甚至“假死”:

  • 点击无响应 ❌
  • 滚动无响应 ❌
  • 动画停滞 ❌
  • 浏览器甚至提示“页面无响应” ⚠️

日志显示,每次接口返回都会触发多次渲染:

setRows(nextRows);        
setTotal(totalCount);     
setSelectedRowKeys(...);  
setLoading(false);        

看起来很正常,但实际上这是问题的开始。


二、根本原因拆解

1. React 16 的同步渲染机制

在 React 16 中:

  • setState 是同步触发渲染
  • 渲染过程是不可中断的
  • 每一次 setState = 一次完整渲染

也就是说:

setRows      → 渲染 #1
setTotal     → 渲染 #2
setSelected  → 渲染 #3
setLoading   → 渲染 #4

👉 4 次 setState = 4 次完整渲染


2. TDesign Table 渲染成本极高

每一次渲染,Table 都会:

  • 重新执行所有 column 的 render
  • 重新创建每个 cell
  • 初始化 Popup 组件
  • 绑定事件处理器
  • 触发浏览器重排 + 重绘

典型调用:

[columns.cell] 渲染 accelerationDomain
[renderDomainWithCopy] 渲染: xxx.com
[columns.cell] 渲染 cname
[columns.cell] 渲染 httpsStatus
[columns.cell] 渲染 actions

👉 所有 cell 都会被重新渲染


3. 时间线(关键证据)

15:10:42 - 渲染 #5 (setTotal)
15:10:43 - 渲染 #6 (setSelectedRowKeys)  → +863ms
15:10:44 - 渲染 #7 (setLoading)          → +933ms

👉 每次渲染耗时 ≈ 800~900ms


4. 为什么会“卡死”而不是“慢”?

这是很多人误解的点。

✅ 本质:JavaScript 是单线程

当 React 开始渲染时:

【主线程被占用】
↓
虚拟 DOM 计算
↓
真实 DOM 更新
↓
Table 渲染
↓
浏览器重排重绘
↓
【800ms 后释放】

这 800ms 内:

  • ❌ 不能点击
  • ❌ 不能滚动
  • ❌ 不能响应任何操作

5. 更致命的是:连续 4 次阻塞

时间轴:

0ms    setRows → 卡 800ms
800ms  setTotal → 卡 800ms
1600ms setSelected → 卡 800ms
2400ms setLoading → 卡 800ms
3200ms 完成

👉 用户感知:页面冻结 3.2 秒


6. 微前端(wujie)的额外放大效应

你的项目运行在微前端环境中,还叠加了:

  • Shadow DOM 隔离
  • 事件代理
  • 主子应用通信

👉 渲染成本进一步放大


三、问题本质总结(一句话)

React 16 同步渲染 + 多次 setState + 高成本 Table 渲染 = 主线程被连续阻塞 → 页面卡死


四、解决方案:批量更新(核心)

使用 React 提供的 API:

import { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() => {
  setRows(nextRows);
  setTotal(totalCount);
  setSelectedRowKeys(...);
  setLoading(false);
});

原理

👉 告诉 React:

  • 把多个 setState 合并
  • 只触发一次 render

性能对比

❌ 优化前(4 次渲染)

渲染 × 4 次
每次 800ms
总耗时 ≈ 3200ms

✅ 优化后(1 次渲染)

渲染 × 1 次
耗时 ≈ 800ms

👉 性能提升 4 倍


五、为什么其他页面也会中招?

这是一个非常常见的反模式

const data = await fetchData();

setState1(data.a);
setState2(data.b);
setState3(data.c);
setLoading(false);

在 React 16 中:

👉 异步回调里的 setState 不会自动合并!


六、React 18 的改进(重要)

如果升级到 React 18:

👉 自动批处理(Automatic Batching)默认开启

await fetch();

setA();
setB();
setC();

👉 自动合并成一次 render


对比

版本行为
React 16❌ 异步 setState 不合并
React 18✅ 自动批处理

七、为什么 React 16 无法避免卡顿?

React 16 的核心限制:

while (有任务) {
  // ❌ 不可中断
  执行渲染();
}

👉 一旦开始渲染:

  • 必须执行完
  • 不能暂停
  • 不能让出主线程

React 18 的优势

  • 可中断渲染
  • 时间切片(Time Slicing)
  • 并发调度

👉 不会长时间阻塞主线程


八、最终总结

为什么会卡死?

  • React 16 同步渲染
  • 多次 setState
  • Table 渲染昂贵
  • JS 单线程阻塞
  • 微前端额外开销

怎么解决?

👉 核心一句话:

unstable_batchedUpdates 把多次 setState 合并成一次渲染


优化效果

指标优化前优化后
渲染次数4 次1 次
卡顿时间3~8 秒0.8~2 秒
用户体验❌ 卡死✅ 可接受

十、一句话经验(最重要)

在 React 16 中,异步里的多个 setState,一定要警惕性能问题。