React 18 并发渲染

如何让紧急交互优先于耗时的 UI 更新?

先记住一句话:并发渲染不是多线程,也不是让 React 在后台开一个线程;它允许 React 在 Render 阶段暂停、重做或放弃低优先级工作,把主线程优先留给更紧急的交互。

并发能力建立在 Fiber 架构之上,通常通过 createRoot 启用 React 18 的新根 API。它不是“所有更新自动变快”,而是让 React 获得更灵活的更新调度能力。

1. 为什么需要并发渲染?

搜索、筛选、图表和大列表等操作可能需要计算大量组件。如果输入更新和结果列表更新被当成同样紧急,列表渲染可能阻塞输入框,导致打字延迟。

并发渲染允许把一次交互拆成不同优先级:

用户输入更新:紧急,应该立即响应
搜索结果更新:非紧急,可以稍后完成

React 可以先提交输入框的新值,暂缓结果列表的 Render;如果用户继续输入,旧的列表计算甚至可以直接放弃,避免把时间浪费在过时结果上。

2. 并发渲染如何工作?

React 仍运行在浏览器 JavaScript 主线程上,并发不等于并行。它主要依靠:

  • Fiber 工作单元;
  • 更新优先级;
  • Scheduler 调度;
  • 可中断、可重做的 Render;
  • Commit 阶段的一致性提交。

可以简化为:

更新进入队列
React 选择更紧急的工作
Render 可暂停、重做或放弃
Commit 只提交最终完成的结果

Render 过程中计算出的中间结果不会直接显示给用户。只有 Commit 后的结果才会成为页面状态,因此组件 Render 必须保持纯粹,不能在里面发送请求、修改 DOM 或执行不可撤销的副作用。

3.startTransition:标记非紧急更新

startTransition 用来告诉 React:回调中的 state 更新不是当前交互必须立刻完成的工作。

import { startTransition, useState } from "react";

function SearchPage({ allItems }) {
  const [input, setInput] = useState("");
  const [query, setQuery] = useState("");

  function handleChange(event) {
    const nextInput = event.target.value;

    // 紧急更新:输入框应立即显示
    setInput(nextInput);

    // 非紧急更新:大列表可以稍后计算
    startTransition(() => {
      setQuery(nextInput);
    });
  }

  const results = allItems.filter((item) => item.includes(query));

  return (
    <>
      <input value={input} onChange={handleChange} />
      <ResultList items={results} />
    </>
  );
}

useTransition 获取 pending 状态

如果需要显示非紧急更新是否仍在进行,可以使用 useTransition

const [isPending, startTransition] = useTransition();

function handleChange(value) {
  startTransition(() => {
    setQuery(value);
  });
}

isPending 适合展示“正在更新结果”的轻量提示,但不要用它替代接口请求本身的 loading 状态。

使用边界

  • 控制输入框当前值的更新通常不能标记为 transition,否则输入响应可能延迟;
  • transition 只改变 React 更新的优先级,不会让一个巨大的同步计算自动变成非阻塞计算;
  • 如果计算本身很重,仍要优化算法、拆分组件、虚拟化列表或移到 Worker;
  • transition 更新可能被打断,因此不要依赖它一定完成某个中间状态。

4.useDeferredValue:延后使用某个值

当你不能直接控制产生值的更新时,可以让某个值在渲染层面“稍后跟上”:

function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  const results = searchItems(deferredQuery);

  return <ResultList items={results} />;
}

它不是传统防抖:

  • 防抖通常使用固定时间延迟,并可能减少请求次数;
  • useDeferredValue 根据 React 当前调度压力决定何时更新;
  • 系统空闲时它可能几乎立即更新,繁忙时则暂时使用旧值;
  • 它不会取消网络请求,也不会自动缓存计算结果。

如果需要明确控制请求频率,仍应使用防抖、缓存或数据请求库;如果需要降低某段 UI 更新的优先级,可以考虑 transition 或 deferred value。

5. Suspense 与并发渲染

Suspense 为 React 能识别的未就绪内容提供 fallback 边界:

<Suspense fallback={<p>加载中...</p>}>
  <LazyPanel />
</Suspense>

常见用途是 React.lazy 代码分割;部分框架和数据层还支持 Suspense 数据请求。并不是任意在 useEffect 中发起的 fetchaxios 请求都会自动触发 Suspense。

在支持 Suspense 的 SSR 环境中,Suspense 还可以配合流式渲染,让已准备好的内容先发送,未准备好的边界稍后补充。具体能力取决于所使用的框架和数据层。

6. 并发渲染与批量更新的区别

两者经常一起出现,但解决的问题不同:

机制解决的问题
批量更新将一批 state 更新集中处理,减少重复 Render / Commit
并发渲染让不同优先级的 Render 可以暂停、切换或放弃
startTransition把某次更新标记为非紧急
useDeferredValue延后某个值在 UI 中的使用

React 18 的 createRoot 同时带来更广泛的自动批处理和并发相关能力,但批量更新本身不等于并发渲染。

7. 和setTimeout 有什么区别?

setTimeout 只是把回调推迟到未来某个时间执行:

setTimeout(() => {
  setQuery(nextQuery);
}, 300);

它适合做明确的时间延迟或防抖,但无法告诉 React 这次更新的优先级,也不能让 React 放弃已经过时的 Render 工作。

startTransition 则是在更新进入 React 后标记其优先级:

  • 不创建后台线程;
  • 不提供固定延迟;
  • 不负责防抖或取消请求;
  • 允许更紧急的更新优先处理;
  • 允许 React 放弃尚未提交的过时结果。

8. React 17 与 React 18 的边界

React 17 没有公开的并发渲染 API,React 18 才通过新根 API 和相关 Hook 对外提供这些能力。但这不表示 React 17 完全不会调度任务,也不表示 React 18 中每次更新都会被中断。

是否能观察到并发效果,取决于:

  • 是否使用 createRoot
  • 更新是否被标记为 transition;
  • Render 工作量是否足够大;
  • 浏览器主线程是否繁忙;
  • 是否使用支持 Suspense 的框架或数据层。

9. 常见误区

并发渲染等于多线程

不是。React 仍主要运行在主线程,通过调度和可中断 Render 让出执行机会。

startTransition 会让代码立即在后台执行

不会。它只是标记更新优先级,具体何时 Render 和 Commit 由 React 调度。

useDeferredValue 是防抖

不是。它没有固定等待时间,也不会自动减少接口请求次数。

transition 中的结果一定会显示一次

不一定。过时的 Render 可能被放弃,只有最终提交的结果才会显示。

并发渲染可以解决所有卡顿

不能。长时间同步计算、过大的 JSON 处理或复杂算法仍会阻塞主线程,需要从计算、数据量、组件拆分和 Worker 等方向解决。

总结

React 18 并发渲染可以概括为:

  1. 它不是多线程,而是可中断、可重做的 Render 调度能力;
  2. startTransition 用于标记非紧急 state 更新;
  3. useDeferredValue 用于延后某个值在 UI 中的使用,不等于防抖;
  4. Suspense 可以为支持的数据或代码加载提供 fallback;
  5. Render 可能被放弃,副作用只能放在事件或 effect 中;
  6. 复杂同步计算仍需要单独优化。

面试时可以这样回答:React 18 的并发渲染建立在 Fiber 之上,不是多线程,而是允许 React 按优先级调度可中断的 Render。用户输入等紧急更新可以优先提交,搜索列表等非紧急更新可以通过 startTransition 延后;useDeferredValue 则允许 UI 暂时使用旧值。只有最终完成的结果会进入 Commit,渲染阶段必须保持纯粹。