React Concurrent Mode 到底解决了什么问题?

445 阅读5分钟

React Concurrent Mode 看起来像一次“架构升级”,实则是对 Web 渲染模型的重构尝试。它不是语法糖,不是性能优化技巧,而是一场调度范式的变革。


01. 页面卡顿的问题到底出在哪?

我们先抛开 Concurrent Mode,看一个大家都见过的 React 性能问题:

<input value={text} onChange={e => setText(e.target.value)} />
{expensiveComputation(text)}

你输入一个字母,页面就卡顿 300ms,CPU 一直飙高。这并不是 React 的错,而是你写了一个同步的、昂贵的计算,它和用户交互耦合在一起,React 的更新流程是阻塞型的

  1. 用户触发 onChange → 触发 setState
  2. React 立刻开始从头开始执行组件树
  3. 所有组件重新执行 → 触发 render → 生成 virtual DOM → diff → commit

如果中间有复杂组件渲染、DOM diff 过程超出 16ms,主线程直接卡住,输入就会延迟、掉帧、跳动、发抖,React 完全无能为力。


02. React 的大脑:调度器的局限性

React v16 之前是同步更新模式:任何 setState 都是立即开始处理,不可打断

这种模式的问题是:

  • 用户输入和 UI 渲染同处一条线程,无法优先响应交互
  • 所有任务一口气干到底,没有“喘息”的机会
  • 没有优先级区分(更新按钮颜色和输入响应都一样)

这本质是调度模型的问题:同步、不可抢占、不可中断。


03. Concurrent Mode:不再“干到底”,而是“做一会,看一看”

Concurrent Mode 改变的是调度系统:

  • 抢占式调度:任务可以中断,优先执行用户交互
  • 时间切片:任务被拆成若干小块,分帧处理
  • 优先级模型:更新被标记不同优先级,点击/输入优先于动画或低优更新
  • 可中断渲染:中途可以暂停,React 不再“一口气跑完组件树”
  • 延迟更新(defer) :可以挂起某些更新,让用户体验更顺滑

这类似操作系统中的调度器,从“批处理”进化为“抢占式多任务”。


04. 举个栗子:input 卡顿如何解决?

经典场景是 input + 慢计算:

<input value={text} onChange={e => setText(e.target.value)} />
<ExpensiveComponent text={text} />

传统模式下,setText 触发后整个页面同步重渲,主线程被锁。

在 Concurrent 模式中,可以这样写:

const [text, setText] = useState('');
const [deferredText, setDeferredText] = useDeferredValue(text); // 🔥

return (
  <>
    <input value={text} onChange={e => setText(e.target.value)} />
    <ExpensiveComponent text={deferredText} />
  </>
);

作用:让 input 响应优先,ExpensiveComponent 稍后再更新。

React 背后使用调度器为两类更新分配了不同优先级:

  • 用户输入(高优)
  • 重渲大型组件(低优)

05. React 到底“并发”了什么?

别被名字误导:React Concurrent Mode 并不是多线程。

它不是把任务放到 Worker,而是通过 requestIdleCallbackMessageChannel 等手段,实现在主线程上的任务切片与调度控制

所以更准确的说法是:

React 并发模式 = 构建在单线程之上的“可中断调度机制”。

它模拟了一种“看起来像并发”的行为:React 可以在执行 render 过程中暂停、中断、恢复,而不是像以前一样“一刀切”。


06. React 并发机制下的新能力

除了响应式输入优化,Concurrent Mode 还开启了很多新可能:

🔹 Suspense for Data Fetching

const data = use(fetchData()); // 会自动挂起,等待加载完成
return <div>{data.title}</div>;

挂起(suspend)意味着:这个组件暂时不渲染,React 会优先处理其他更新或 fallback。

这要求 render 函数本身能被挂起/恢复,而不是一旦开始就必须跑完——这就是并发模式的基石。


🔹 Transitions

用户点击按钮跳转页面,但你希望保留当前视图直到加载完成:

const [isPending, startTransition] = useTransition();

startTransition(() => {
  setRoute('/profile'); // 低优更新,允许中断
});

这种模式下:

  • 点击事件处理是高优
  • 页面跳转是可延迟的
  • 加载中可以显示旧页面 + loading indicator

07. Cold Knowledge:为什么 Concurrent 模式迟迟没有默认启用?

虽然 React 18 开始默认启用部分并发特性(如自动批处理),但并没有默认开启完整的 Concurrent Mode(需要 <Suspense>useTransition 等)

这是因为:

  1. 兼容性要求高:你的组件必须可中断、无副作用(比如在 render 中调用 Date.now() 会出问题)
  2. 旧生态不支持:大量 UI 库还未适配 concurrent 特性,像 Portal、Context、动画库
  3. 调试难度陡增:中断和恢复让开发者很难追踪 render 的执行流程
  4. 心理模型转变难:开发者习惯同步渲染,对于挂起、恢复、优先级不够敏感

一句话:Concurrent 是未来,但今天的大多数项目还没准备好。


08. 总结:React Concurrent Mode 解决了什么?

✅ 解决了主线程卡顿问题
✅ 提供了更好的交互体验(响应快)
✅ 支持数据请求的渐进式加载
✅ 开启了“时间切片”与优先级调度的前端范式

但也带来了新的挑战:

  • 新的心理模型
  • 更复杂的调度机制
  • 对组件可中断性的要求

09. 再想一步:如果没有 Concurrent Mode,我们能否自己实现?

是的,社区已有一些“并发渲染”尝试,例如:

  • preact signals
  • Solid.js 的 fine-grained update
  • Vue 3 中的 Suspense

但 React 是第一个系统性提出“调度优先级+中断恢复”的大厂实现,它对前端应用结构本身提出了新解法。

未来的 React 应用不会是“响应式 UI”,而是“带调度能力的协作式 UI”。


结语

React Concurrent Mode 是一次对前端渲染模型的重写,它不是“提升性能”这么简单,而是给予开发者更强的控制权:你可以明确告诉 React 哪个更新重要,哪个可以延后。

UI 不再是被动地等待数据和计算完成,而是主动调度,优雅降级,渐进展示。

它的难,不是技术,而是“需要你重新理解组件的生命周期、状态流转、更新机制”。

我们习惯了“组件就是函数”,但 Concurrent Mode 说:组件其实是一段可能暂停、重试、恢复的过程

这就是它要解决的问题:前端不再只是“渲染”,而是“调度”。