一句话
Concurrent Rendering 让你标记某些状态更新为「非关键」,使 React 在「后台」处理它们而不阻塞 UI——但会用两次重渲染作为代价。
问题:慢状态更新冻结一切
很多人以为状态更新是异步的。不是。 触发虽然是异步的(来自回调),但一旦触发,React 会同步地计算所有重渲染、提交到 DOM,然后才把控制权还给浏览器。
如果一个重型组件渲染要 1 秒,这 1 秒内整个 UI 冻结,用户点击被排队等主任务结束。
Concurrent Rendering 的本质
传统模式:
setState → 同步渲染(阻塞主线程)→ 渲染完成 → 浏览器响应
Concurrent 模式:
startTransition(setState) → 后台渲染(定期检查主队列)
↓ 如果有「关键」更新进来
→ 暂停后台工作 → 处理关键更新 → 再继续/放弃
"后台"是心智模型——JS 是单线程的,React 只是在工作间隙检查主队列。
useTransition 用法
const [isPending, startTransition] = useTransition();
// 包裹状态更新,标记为非关键
startTransition(() => {
setTab('projects');
});
startTransition:包裹状态更新,标记为非关键isPending:布尔值,表示过渡正在进行中(可用于 loading 指示器)- 关键更新(普通 setState)可以中断过渡
⚠️ 核心陷阱:双重重渲染
useTransition 会导致两次重渲染:
- 立即的「关键」重渲染——旧 state 不变,只是
isPending变为true - 后台的「非关键」更新——state 才真正改变
后果:如果所有 tab 切换都用 startTransition,从一个重型页面点走时,第一次重渲染仍然会渲染那个重型页面(旧 state),反而比不用更慢。
正确用法
✅ 策略一:确保所有受影响组件已 memo 化
const ProjectsMemo = React.memo(Projects);
// 确保重型组件被 memo 包裹 → 第一次重渲染时跳过重型计算
前提条件(全部满足才有效):
- 所有重型组件用
React.memo包裹 - 所有 props 用
useMemo/useCallbackmemo 化 isPending不要作为 prop 传给 memo 组件
✅ 策略二:只从「轻」到「重」时使用
// ✅ 从 loading 空状态 → 重型数据页(第一次重渲染是轻量的)
useEffect(() => {
fetch('/api').then(data => {
startTransition(() => setData(data));
});
}, []);
不要从重型 → 重型使用。
useDeferredValue
const TabContent = ({ tab }) => {
const tabDeferred = useDeferredValue(tab);
// 用 tabDeferred 渲染,更新被延迟
};
- 适用场景:拿不到 setState 函数(值来自 props)
- 同样存在双重重渲染问题
- 同样的解决方案
❌ 不能替代 debounce
用 useTransition 做 input 防抖不生效——React 太快了,能在两次按键间就完成后台计算和提交,每个值变化都会触发。
决策树
需要 useTransition 吗?
├─ 有重型渲染阻塞 UI?→ 继续
│ ├─ 能 memo 化所有受影响组件?→ ✅ 可以用
│ ├─ 是从「轻」→「重」过渡?→ ✅ 可以用
│ └─ 两个都不满足?→ ❌ 别用,会变慢
├─ 想替代 debounce?→ ❌ 用 lodash.debounce
└─ 所有状态更新都包一层?→ ❌ 绝对不要