【翻译】从 Fiber 到异步 React

7 阅读21分钟

从 Fiber 到异步 React

副标题:现代 React 背后缺失的心智模型

原文页头可见文本(保留):

  • Latest / Snippets / About
  • Clueless Words icon
  • Home / react

距离 React 19 发布已有一段时间,此后我们见到了可以加入应用中的新 API、新 Hook 和新组件。有人会说这些变化很受欢迎,也有人觉得这些新增能力很酷,但意义何在?

不妨先从一个问题开始:在 React 里,我们要如何做数据请求,或任何异步操作? 当然,我们可以选用 TanStack Query、SWR 等数据请求/异步库——但假设我们没有这些库,又该怎么办?

下面的代码片段你可能并不陌生:

const UsersList = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch("/api/users");

        if (!response.ok) {
          throw new Error("Failed to fetch users");
        }

        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);
};

传统上,我们常用 useEffect 在 React 应用里处理异步工作(包括数据请求)。上面这个组件在挂载到 DOM 时调用 fetchUsers,并分别设置 data、loading、error,以便向用户展示合适的 UI。

这种做法能跑起来,但正如社区与 React 核心团队多次指出的:它有不少缺陷,并不是在 React 中处理异步工作的推荐方式。

事实上,React 一直在与异步打交道,即便它并未显式建模。数据请求、代码分割、用户输入、动画、导航都是异步发生的,然而在 React 历史的很长一段时间里,这些关注点都活在渲染系统之外,而不是之内

于是我们要自己把 loading、effect、命令式更新拼在一起,才能在异步完成时保持 UI 一致。但这本该是应用开发者的责任吗?还是应该由别处承担?

再想一想:如果异步工作活在渲染系统之内,而不是环绕着它,代码库会是什么样子? 它又会如何改变我们构建组件、组件体系乃至整个应用的方式?

React 19 的发布,补全了 「Async React」 这一叙事——本文会逐步拆解这个词。我们会看 Async React 如何解决 UI 与异步工作之间的协同问题,并尝试回答上面的问题。也会触及新问题:在 Async React 已经到来的前提下,库作者应如何构建、组合他们提供给应用开发者的工具?

对开发者来说,组织 React 应用的方式正在进入一个全新阶段,这些变化也能扩展我们能创造的产品形态。全文会有不少深度段落,在阅读现代 React 的构建方式时,可以把下面几点记在心上。

React 协调器(Reconciler)

创建 React Web 应用时,你常会注意到两个重要包:ReactReact-dom。React 让你把 UI 描述为状态的函数,我们常写作 UI = f(state),并用 JSX 来表达。

react-dom 扮演什么角色?

react-dom 包含面向 Web 的 React 协调器(reconciler)渲染器(renderer)。协调器负责随时间比较渲染输出、调度工作,并确定要在宿主平台1上应用的最小更新集合;渲染器则把协调器的输出应用到宿主——此处即 DOM。

讨论 Async React 以及新 API 何以可能时,协调器至关重要。在 React Conf 2017 上曾透露:React 的协调器被彻底重写,从所谓的 stack reconciler(栈协调器) 演进为 React Fiber

栈协调器(Stack Reconciler)

栈协调器是 React 16 之前早期版本使用的协调引擎。它的设计刻意贴近 JavaScript 的函数调用方式,因此直接依赖调用栈来遍历并渲染组件树。

当渲染被触发——通常由状态变化引起——React 会从根节点开始,递归地自上而下遍历组件树,调用每个组件的 render,做 diff,然后提交结果。这意味着一旦协调/渲染开始,React 必须跑完整个过程,才能把控制权交还给浏览器。

(原文含可交互演示:点击 “Start render” 观察 dashboard 中 count 的更新过程;此处为静态译文,完整交互见原文页面。)

该演示中的关键界面文案(按原文保留):

  • AppLayout / Dashboard / count: 0 / Sidebar / SidebarItem / Footer
  • idle / render / commit
  • Browser Main Thread
  • Ready
  • Responsive to user input
  • Start render / Reset Component Tree

这种方式完全同步、不可打断:所有渲染工作在一趟里完成,所有更新优先级相同。系统简单可预测,但也意味着长渲染会阻塞主线程、拖慢输入,在复杂应用里造成明显卡顿。

在栈协调器的局限之下,我们能否想象一种可改进的世界?

随堂测验(原文为交互): 在栈协调器下,React 无法暂停工作、无法重新排定更新优先级、也无法放弃已不再相关的渲染。—— 见原文页面作答。

原文交互项:True / False / Submit

React Fiber

React Fiber 是对 React 协调器的完全重写,在 React 16 引入,用以解决基于栈的方案的限制。Fiber 不再用单次不间断的大渲染,而是建立组件树自己的内部表示——Fiber 树——以及自己的调度模型。

在 Fiber 中,每个组件是一个可独立处理的工作单元(fiber)。渲染被拆成许多小步,React 可以暂停让出给浏览器2、稍后继续。因此 Fiber 把 协调/渲染阶段提交(commit)阶段 拆成两个独立过程。

批注:原文此处为独立小标题 The Fiber Node(非普通正文句)。

Fiber 节点

Fiber 节点是一个轻量对象,描述与组件相关的元数据。每个 Fiber 含有指向父、子、兄弟的指针。下面是一个极度简化的示例,完整实现见 React 源码:

const fiberNode = {
  tag: "",
  key: "key",
  type: null,

  return: null,
  child: null,
  sibling: null,
  ref: null,

  pendingProps: null,
  memoizedProps: null,
  updateQueue: null,
  memoisedState: null,
};

每个 Fiber 是一个离散工作单元,React 可以一次处理一个节点,而不必依赖必须跑完的递归调用。单元之间,React 可以判断是继续渲染还是把控制权交回浏览器。若要暂停,React 停止遍历、在 Fiber 树中保留位置并 yield;恢复时从下一个 Fiber 继续,仿佛未曾中断。

这一过程即 time slicing(时间切片),是把渲染工作建模为 JavaScript 对象的直接结果。

在协调/渲染阶段(现称 reconciliation phase),会生成待渲染到 UI 的变更列表,但尚未提交到 DOM;变更被安排在下一阶段提交。提交阶段(commit phase) 才把这些变更落到 DOM。

要点:协调阶段可以被打断、暂停/恢复、完全丢弃提交阶段不能打断——一旦开始,必须完成才能做其他事。

随堂测验(原文为交互): Fiber 可以随时间准备多版 UI,但只对 DOM 提交最终变更,从而避免不必要的 DOM 更新。—— 见原文。

原文交互项:True / False / Submit

由于协调阶段可打断,两段独立工作可以都在协调阶段内完成,再以不同顺序进入提交阶段。这让 Fiber 在「何时把渲染更新显示到 DOM」上引入了优先级概念。

Fiber 让 React 先处理高优先级更新、延后低优先级更新,从而改善感知性能与体验。例如:在输入框里打字,比「后台刚加载完的数据」更紧急——那就先保证输入框状态立刻可见,数据渲染可以等用户打完再跟上。这就是所谓更好的感知性能

Fiber 的优先级大致包括:

  • 同步工作(点击/输入等——行为类似栈协调器)
  • Task 工作
  • 动画(由 requestAnimationFrame() 启动)
  • 高优先级
  • 低优先级
  • 离屏(当前不可见但预渲染会有帮助的内容)

在讨论 React 18、19 引入的 API、Hook 与组件时,这些优先级组会再次派上用场。核心结论是:没有迁到 Fiber,就不会有 Async React 的概念。

批注:原文边栏/小节标题为 More on the reconciler(以下「更多阅读」即对应该部分的展开)。

更多阅读

想深入了解协调器与 Fiber,可看 Lin Clark 的演讲:A Cartoon Intro to Fiber,或 Brandon 的演讲:Algebraic effects, Fibers, Coroutines Oh my!

与 Fiber 协调器密切相关的浏览器 API 还有:requestIdleCallbackrequestAnimationFrame

现代 React 特性

Fiber 是工程上的里程碑:可中断渲染、协作式调度、优先级等等。有了这些概念,我们才能想象构建 React 应用的新世界。但 Fiber 发布时缺了让开发者接入这套新范式的 API。

批注:原文在段前使用独立小标题 APIs definition(提示作者将说明「API」一词的两种用法;不应并入上一段当作续写)。

关于「API」一词

文中我宽松地用 API 指 Hook、函数、组件等;而 React 对 API 也有正式定义(指 React 包导出的函数)。下文会区分「广义 API」与「狭义 React API」。

React 18 带来的 API,让开发者终于能用到 Fiber 引入的部分概念。注意我用了部分——直到 React 19(尤其是 19.2),开发者才拿到剩余 API,完整解锁 Fiber 的能力。

例如:

  • Suspense
  • startTransition(API)与 useTransition
  • useDeferredValue
  • useOptimistic
  • useFormStatus
  • useActionState
  • use(API)
  • Activity
  • View Transitions(实验性)

这些在 React 文档中有详尽说明,也有不少教程。下面只挑几样与 Fiber 的关系。

Fiber 与 transition 的关系

前面说过,迁到 Fiber 是 transition 存在的前提。用栈协调器可以看一个对比示例(原文含交互:先加低优先级任务再加高优先级,观察何时渲染到屏幕)。

该示例中的界面文案(按原文保留):

  • Add Task
  • Low priority Task / High priority Task
  • Reset
  • Pending / Processing / Completed

在栈协调器下,无论优先级如何,项出现在屏幕上的时机没有区别。当混合了本应高/低优先级的更新时,渲染顺序往往遵循后进先出。这带来问题:若许多低优先级更新排在高优先级之后,在栈协调器世界里,高优先级更新要等低优先级全部渲完才能上屏——无法按优先级调度渲染工作。

Fiber 通过优先级解决了这一点:先调度高优先级,再处理低优先级。换 Fiber 后再看同一示例(原文第二个交互),差异很明显:高优先级总是先渲染;若正在渲低优先级时来了高优先级,会暂停低优先级、先完成高优先级,再继续低优先级。

第二个示例同样包含:Add Task / Low priority Task / High priority Task / Reset / Pending / Processing / Completed

实现这种更新优先级调度的 API 就是 transition,尤其是 startTransition APIuseTransition Hook:它们把一组更新标为较低优先级,让 React 知道可以延后到高优先级工作完成之后。

Fiber 与 Suspense 的关系

Fiber 带来了可中断渲染,因此 React 可以暂停/恢复渲染,并丢弃尚未提交到 DOM 的工作。

这也深刻影响了 Suspense:在子节点加载完成前显示 fallback UI。当组件在协调阶段 throw 一个 Promise 时,称该组件被 suspend(挂起)。React 不把它当错误,而把 thrown promise 理解为「该组件尚不能完成渲染」。最近的 Suspense 边界会捕获这个 promise,暂停该子树的渲染,转而显示 fallback;直到 promise resolve,子树渲染才继续。

之所以能这样做,是因为 Fiber 允许在协调阶段打断渲染:此时还没有任何 DOM 变更,部分完成的工作可以安全放弃。

(原文含 Suspense 演示:点击 “Render Components”;完整交互见原文。)

点击后 Suspended component 开始渲染,在渲染中 throw 一个 3 秒后 resolve 的 promise;pending 期间 Suspense 显示 fallback(「Loading」);resolve 后继续渲染并显示最终内容。

其中机制在 Lin Clark 的演讲中有更细讲解。要点:没有可中断渲染,就不会有 Suspense。

Async React

至此,我们看了协调器的角色、从栈协调器到 Fiber 的架构转变,以及 Fiber 如何支撑今天的 Suspense、transition、乐观更新与 Actions。

更要强调的是 我们如何走到今天:协调器重写为异步/并发渲染打下了基础,但建在其上的特性是分多个版本逐步出现的。

结果是:这些特性长期被单独文档化、教学与采用。Suspense 被说成加载机制transition 被说成性能优化乐观更新用于 UXActions 偏向服务端表单——这些说法都不算错,但都忽略了更大的图景。

这些 API 不是彼此割裂的点子,而是 Fiber 所实现的同一套底层模型的不同表达协同的、带优先级的、异步/并发渲染

可以逐步采用这些特性,但代码容易变成混合架构,无法充分享受 Async React:代码能跑,心智模型却支离破碎,复杂度上来后越来越难推理。

异步优先(async first)的思维

构建现代 React 应用,需要调整我们看待架构的方式。

异步优先指:用声明式工具表达用户意图、加载状态、优先级与视觉连续性,让 React 把渲染视为可调度、可中断的操作,并由 React 自己协调。

我们与用户之间形成一种契约:用户立刻操作时,UI 立刻响应,异步工作在后台进行;数据就绪且没有更高优先级工作时,再以协同的方式展示完成态。

Ricky Hanlon 在 React Conf 2025 的演讲 Async React 中对这一过程做了建模(Event → busy → Update → loading → Render → done → Commit)。

(原文含类 Instagram 演示:渲染视图、点赞、点踩、归档/取消归档、切换标签等;见原文。)

演示入口文案:Render View

这个 demo 用乐观状态、transition、Suspense、Activity 等组合实现。你可能注意到切换标签很快;但若先归档再立刻切标签,会乐观地切到归档标签并在标签区域显示 loading;由于归档页渲染尚未就绪,React 仍把体验留在 feed;异步数据就绪后,再完整切换到归档页。

所有这些协同都由 React 处理,我们只定义交互应呈现的样子。demo 里没有用 useEffect 处理任何异步工作。 那异步优先组件怎么写?

一个异步优先组件

在异步优先组件里,我们可以假设组件渲染时已经具备所需数据。

我们知道可用 Suspense 在 loading fallback 与挂起组件之间协调。但若依赖 Suspense 让 React 协同,出错时呢?promise reject 时呢?

这时需要 Error Boundary。Suspense 在 promise 被 throw 时显示 fallback;Error Boundary 在组件 throw 错误 时显示 fallback。协调仍由 React 完成,我们提供 fallback UI 以及可能的重置方式。

Error Boundary 尚未以函数组件一等公民的方式演进,但在 Async React 里它非常关键。可以手写,也可用 react-error-boundary 等库。

这种模型里我们确实在做假设,但假设之中是 React 亲自协调的工作。可以说 「异步」建在 React 渲染系统之内,而不是环绕它。问题从「如何随时间协调异步?」变成「React 应如何协调?」

实践上可以这样写:

const UsersList = ({ userDataPromise }) => {
  const userList = use(userDataPromise);

  return (
    <div>
      {userList.map((user) => (
        <p key={user.id}>{user.name}</p>
      ))}
    </div>
  );
};

const AsyncFirstDemo = ({ userDataPromise }) => {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <Suspense fallback={<LoadingFallback />}>
        <UsersList userDataPromise={userDataPromise} />
      </Suspense>
    </ErrorBoundary>
  );
};

还记得最前面的 useEffect 请求示例吗?这样是不是简单得多?异步优先组件把 React 的核心理念带到了「数据不能立刻就绪」的世界:我们向 React 描述组件长什么样、如何表现,协调交给 React

上文刻意没写数据请求逻辑,重点在组件形态。数据可以通过 Server Components、数据请求库、或手动请求再把 promise 往下传等方式获取。

TanStack Query 等库也已拥抱这种模型,提供返回 promise 的 Hook,可直接用于异步优先组件。当然也可以自己写返回 promise 的请求逻辑。

是的,我们反复说 promise,因为协调不是我们在写:无论 pending、fulfilled 还是 rejected,都是 React 在处理。

(原文有「Show more」折叠与更多演示,见原文。)

异步 React 下的组件体系与路由

这是一种真正的范式转变。乍一看像在拿简单性换抽象:为了声明式、异步优先,要引入 Actions、Suspense/Error Boundary、transition、乐观状态等新概念。

自然会问:我们现在就该这样构建应用吗? 简短回答:是,也不完全是——要更细地看。

异步优先的组件库

我们往往会使用组件库/体系:一套可复用、可组合的 UI,封装设计、行为与无障碍。这些库也应能参与异步优先模型。

这里 Action props 很关键:把一个同步/异步函数包在 transition 里再作为 prop 传入,小改动、大影响,例如:

<Button action={saveUserAction}>Save</Button>

通过 actionButton 可以自动反映自身在渲染生命周期中的状态:pending、disabled、默认防重复点击、把错误抛给最近边界等;使用者只需声明按钮做什么

原生元素也在朝这个方向走,例如 form 可传 action(同步/异步函数)而不仅是 onSubmit,子组件可获得该 transition 的 pending 状态或返回值。异步优先组件库只是同一思想的延伸。

本质上,异步优先组件是 意图驱动(intent-driven):我们声明 UI 该做什么,而不是为每次异步操作重复样板。我们又在抽象层级上迈了一步

支持 Suspense 的路由

把异步优先组件、异步组件体系与 支持 Suspense 的路由结合,就是在应用层面拥抱异步优先架构。

支持 Suspense 的路由能原生处理可能在加载数据或代码时挂起的组件:自动在 Suspense 边界内渲染路由,在路由就绪前显示约定的 fallback。我们仍要定义页面级 fallback(骨架屏、转圈等),但何时展示由路由配合完成;也可定义错误 fallback。

这也意味着我们选择了基于 React 异步渲染策略的导航与数据方案:导航被包在 transition 里,视为低优先级更新,React 保持当前 UI 可响应、可见,同时准备下一路由。路由组件因数据或代码挂起时,transition 能减少不必要的 loading 闪烁。

(原文含标签页切换演示与社区集成示例,见原文。)

原文该段出现的主要界面文案(按原文保留):

  • Community Integrations
  • Installed / Explore / Recommended
  • React Router V7
  • Async Router
  • Connected
  • Notifications
  • Suspense enabled Notifications

点击 Explore 时导航栏可出现 pending,后台准备组件,就绪后再切换。关键是:准备下一路由时,当前 UI 仍可见可交互。若从 Explore 快速切到 Recommended,会取消 Explore 的准备转而准备 Recommended,导航体验更顺滑。

这些机制由各路由的 API 暴露,实现不同,但接入 Suspense 与 transition 的核心逻辑一致。

哪里能找到这类路由?

例如 Next.js App Router 提供支持 Suspense 的路由方案:可在路由 API 里为 Suspense 与 Error Boundary 定义 fallback,挂起组件由框架处理;基于文件路由时,用 loading.tsxerror.tsx 即可。

React Router v7 的 framework/data 模式也支持,但它不是纯 Suspense 驱动路由:导航都包在 transition 里(低优先级),但不会自动给路由树里所有组件包 Suspense——迁入异步优先世界时要留意,仍可手动用 Suspense 包裹路由以显示合适 fallback。

这些拼在一起很妙:异步优先组件 + 异步优先组件库/体系 + 支持 Suspense 的路由,让应用把 async 当作默认

收尾

Async React——十年磨一剑,它来了。这是了不起的工程成就,也是今天构建 React 应用的巨大心智转变

一路走来,Async React 并非偶然:协调器从栈模型迁到 Fiber,使我们能在任何更新提交到 DOM 之前调度、打断、排序、放弃渲染;我们也看清了 Fiber 在 Suspense、transition 等机制背后的作用。

于是 React 18/19 的 API 不是孤立技巧,而是 Fiber 所允许的统一心智模型的不同侧面。

Async React 到底是什么?

它是一种 以异步为默认 的思维方式与构建方式,而不是把异步当成边缘情形。由 React 协调「何时、如何」更新 UI,而不是让每个组件自己管理依赖。组件可以假设数据已就绪,未就绪就挂起,自动恢复,无需手写 loading 与生命周期编排。我们写更声明式的代码,把数据请求、渲染与用户交互的复杂度交给 React,以响应式、非阻塞的方式协同。

我们通过 transition 显式包裹低优先级更新来扩展这一模型,让紧急交互保持灵敏,非关键 UI 在后台准备。但随之而来的问题是:是否所有状态更新都要包进 transition?

实践中:。transition 针对非紧急更新;响应输入、打字、指针反馈等应保持同步,界面才够跟手。若全部包进 transition,会模糊界限,拖慢用户期望立即看到的反馈。

就像语义化 HTML,对 transition 的使用也要有语义与自觉,形成「何时用、何时不用」的共同语言。

这也适用于异步优先组件库。文中说 Action props「应当」参与异步优先模型,不是要用 action 取代 库组件暴露的 onClick,而是 actiononClick 并存,让使用者选择同步高优先级更新,或带 pending/禁用/错误态的异步低优先级更新。语义也会随实践演化。

最后,支持 Suspense 的路由原生支持在加载数据或代码时可能挂起的组件,在 Suspense 边界内渲染路由并显示 fallback;我们仍定义 loading 与 error UI 长什么样,何时出现由路由与 React 协同。导航包在 transition 里,准备下一屏时保持当前 UI 可见可点,减少突兀跳转。

更妙的是,其中不少思想不限于 React Web:协调器与平台无关,React Native 等渲染器同样可以走向异步优先、跨平台的构建方式。

和同事聊到时我们也在想:如果这些 API 一次性全给齐,会不会更容易讲清 Async React 的故事?我不确定——但这确实是一个很酷的故事;既然能力已经齐备,值得继续讨论 Async React,并在架构新体验时把这些概念放在心上。

留给你的问题:对编写 Hook、组件与工具的库作者来说,Async React 意味着什么?我们期望这些库如何参与异步优先模型?已有库在探索,但仍有许多未解之问。

就写到这里。希望对你有用,下篇再见……Peace!

练习题

嘘!嘿你!对,就是你! 喜欢这篇文章吗?这里有个小练习可以试试 👀

练习(Exercise)

这是一组和本文配套的问题/小游戏,用来帮助你巩固文中概念。玩得开心!

开始练习(Begin Exercise)

本页目录(on this page)

  • the react reconciler
  • stack reconciler
  • react fiber
  • modern react features
  • fibers importance with transitions
  • fibers importance with suspense
  • async react
  • think async first
  • an async first component
  • component systems and routers with async react
  • async first component libraries
  • suspense enabled routers
  • wrapping up
  • practice problems

  • 原文页最后更新:2026-02-03(Last updated February 3rd, 2026
  • 译文整理日期:2026-04-17(见 front matter processed_at
  • 作者:Nonso O.
  • 致谢Thanks for reading!
  • 阅读时长Read Time
  • 社交链接Twitter / GitHub / YouTube
  • 互动数据Like / 4.2K views / 12

原文页尾原始串联文本(保留):

  • TwitterGitHubYoutube

校对说明:已补齐原文末尾信息与页面内目录;星号脚注([^fn-host][^fn-yield])与「APIs definition」「The Fiber Node」「More on the reconciler」等边注已单独标为批注/脚注,未并入正文。逐段严格对照见同目录 从-fiber-到异步-react.逐段对照.md

Footnotes

  1. 批注(对应原文 host platform*:「宿主平台」指 reconciler 要把更新应用到的目标运行时环境。本文语境下 react-dom 的宿主是 DOM;若换用 React Native 等 renderer,宿主则为对应原生视图层。若原文页脚注措辞与本文不同,以 原文页面 为准。

  2. 批注(对应原文 yield to the browser *:指 Fiber 将渲染拆成小步后,可在步间把主线程控制权交还给浏览器,以便处理输入、布局与绘制等,再继续 React 的工作(与时间切片/并发调度相关)。若原文页脚注措辞与本文不同,以 原文页面 为准。