跳到主要内容

新架构已来

·阅读24分钟
The React Team
React 团队
@reactjs / @reactnative

默认启用新架构的 React Native 0.76 现已在 npm 上发布!

0.76 版本博客文章中,我们分享了此版本中包含的重大更改列表。在这篇文章中,我们将概述新架构以及它如何塑造 React Native 的未来。

新架构全面支持现代 React 功能,包括SuspenseTransitions自动批处理useLayoutEffect。新架构还包括新的原生模块原生组件系统,让您无需桥接即可编写类型安全的代码并直接访问原生接口。

此版本是我们自 2018 年以来一直致力于的 React Native 彻底重写的结果,我们特别注意使新架构对大多数应用程序来说是一个渐进式迁移。2021 年,我们创建了新架构工作组,与社区合作,确保整个 React 生态系统获得流畅的升级体验。

大多数应用程序将能够以与其他版本相同的努力程度采用 React Native 0.76。最流行的 React Native 库已经支持新架构。新架构还包括一个自动互操作层,以实现与针对旧架构的库的向后兼容性。

在过去几年的开发中,我们的团队公开分享了我们对新架构的愿景。如果您错过了其中任何一场演讲,请在此处查看

什么是新架构

新架构是对 React Native 基础主要系统的全面重写,包括组件的渲染方式、JavaScript 抽象与原生抽象的通信方式以及跨不同线程的任务调度方式。尽管大多数用户无需考虑这些系统的工作方式,但这些更改带来了改进和新功能。

在旧架构中,React Native 使用异步桥与原生平台通信。为了渲染组件或调用原生函数,React Native 需要通过桥接序列化和排队原生函数调用,这些调用将异步处理。这种架构的好处是主线程永远不会因渲染更新或处理原生模块函数调用而被阻塞,因为所有工作都在后台线程上完成。

然而,用户期望对交互做出即时反馈,以使其感觉像一个原生应用程序。这意味着某些更新需要同步渲染以响应用户输入,这可能会中断任何正在进行的渲染。由于旧架构只是异步的,我们需要重写它以允许异步和同步更新。

此外,在旧架构中,通过桥接序列化函数调用很快成为瓶颈,特别是对于频繁更新或大型对象。这使得应用程序难以可靠地实现 60+ FPS。还存在同步问题:当 JavaScript 和原生层不同步时,无法同步协调它们,导致列表显示空白帧和由于中间状态渲染而导致的视觉 UI 跳动等错误。

最后,由于旧架构使用原生层次结构保留了 UI 的单个副本,并就地修改了该副本,因此布局只能在一个线程上计算。这使得处理用户输入等紧急更新成为不可能,并且无法同步读取布局,例如在布局效果中读取以更新工具提示的位置。

所有这些问题都意味着无法正确支持 React 的并发功能。为了解决这些问题,新架构包含四个主要部分

  • 新的原生模块系统
  • 新的渲染器
  • 事件循环
  • 移除桥接

新的模块系统允许 React Native 渲染器同步访问原生层,从而使其能够异步和同步处理事件、调度更新和读取布局。新的原生模块默认也延迟加载,为应用程序带来显著的性能提升。

新的渲染器可以跨多个线程处理多个进行中的树,这允许 React 处理多个并发更新优先级,无论是在主线程还是后台线程。它还支持同步或异步地从多个线程读取布局,以支持更灵敏的 UI 而不卡顿。

新的事件循环可以按明确定义的顺序在 JavaScript 线程上处理任务。这允许 React 中断渲染以处理事件,因此紧急用户事件可以优先于较低优先级的 UI 过渡。事件循环还与 Web 规范对齐,因此我们可以支持微任务、MutationObserverIntersectionObserver 等浏览器功能。

最后,移除桥接可以加快启动速度,并实现 JavaScript 和原生运行时之间的直接通信,从而最大限度地降低切换工作的成本。这还可以改进错误报告、调试,并减少因未定义行为导致的崩溃。

新架构现已准备好投入生产使用。它已经在 Meta 的 Facebook 应用程序和其他产品中大规模使用。我们成功地在为Quest 设备开发的 Facebook 和 Instagram 应用程序中使用了 React Native 和新架构。

我们的合作伙伴已经使用新架构数月:请查看 ExpensifyKraken 的成功案例,并尝试 Bluesky 的新版本。

新原生模块

新的原生模块系统是对 JavaScript 和原生平台通信方式的重大重写。它完全用 C++ 编写,这解锁了许多新功能

  • 与原生运行时同步访问
  • JavaScript 和原生代码之间的类型安全
  • 跨平台代码共享
  • 默认延迟模块加载

在新的原生模块系统中,JavaScript 和原生层现在可以通过 JavaScript 接口 (JSI) 同步通信,而无需使用异步桥。这意味着您的自定义原生模块现在可以同步调用函数、返回值,并将该值传递回另一个原生模块函数。

在旧架构中,为了处理原生函数调用的响应,您需要提供一个回调,并且返回的值需要可序列化

// ❌ Sync callback from Native Module
nativeModule.getValue(value => {
// ❌ value cannot reference a native object
nativeModule.doSomething(value);
});

在新架构中,您可以同步调用原生函数

// ✅ Sync response from Native Module
const value = nativeModule.getValue();

// ✅ value can be a reference to a native object
nativeModule.doSomething(value);

通过新架构,您终于可以充分利用 C++ 原生实现的强大功能,同时仍然可以从 JavaScript/TypeScript API 访问它。新模块系统支持用 C++ 编写的模块,因此您可以一次编写模块,它可以在所有平台(包括 Android、iOS、Windows 和 macOS)上运行。用 C++ 实现模块可以实现更细粒度的内存管理和性能优化。

此外,借助 Codegen,您的模块可以定义 JavaScript 层和原生层之间的强类型契约。根据我们的经验,跨边界类型错误是跨平台应用程序中最常见的崩溃源之一。Codegen 允许您克服这些问题,同时为您生成样板代码。

最后,模块现在是懒加载的:它们仅在实际需要时才加载到内存中,而不是在启动时加载。这减少了应用程序启动时间,并随着应用程序复杂性的增加而保持较低的启动时间。

诸如 react-native-mmkv 等流行库已经从迁移到新原生模块中获得了好处

“新的原生模块极大地简化了 react-native-mmkv 的设置、自动链接和初始化。得益于新架构,react-native-mmkv 现在是一个纯 C++ 原生模块,这使其可以在任何平台上运行。新的 Codegen 允许 MMKV 完全类型安全,这通过强制执行空安全修复了一个长期存在的 NullPointerReference 问题,并且能够同步调用原生模块函数使我们能够用新的原生模块 API 替换自定义 JSI 访问。”

Marc Rousavyreact-native-mmkv 的创建者

新渲染器

我们还彻底重写了原生渲染器,增加了多项优势

  • 更新可以在不同线程上以不同优先级渲染。
  • 布局可以同步并在不同线程之间读取。
  • 渲染器用 C++ 编写,并在所有平台上共享。

更新后的原生渲染器现在将视图层次结构存储在不可变树结构中。这意味着 UI 以无法直接更改的方式存储,从而允许线程安全地处理更新。这使其能够处理多个进行中的树,每个树都代表用户界面的不同版本。因此,更新可以在后台渲染而不会阻塞 UI(例如在过渡期间)或在主线程上渲染(响应用户输入)。

通过支持多线程,React 可以中断低优先级更新以渲染紧急更新(例如由用户输入生成的更新),然后根据需要恢复低优先级更新。新渲染器还可以同步地跨不同线程读取布局信息。这使得低优先级更新的后台计算以及在需要时(例如重新定位工具提示)的同步读取成为可能。

最后,用 C++ 重写渲染器使其可以在所有平台之间共享。这确保了相同的代码在 iOS、Android、Windows、macOS 和任何其他 React Native 支持的平台上运行,提供一致的渲染功能,而无需为每个平台重新实现。

这是我们实现多平台愿景的重要一步。例如,视图扁平化是一种仅适用于 Android 的优化,以避免深层布局树。新的渲染器,凭借共享的 C++ 核心,将此功能带到了 iOS。将此功能带到 iOS。此优化是自动的,不需要设置,它随共享渲染器免费提供。

通过这些更改,React Native 现在完全支持并发 React 功能,如 Suspense 和 Transitions,从而更容易构建复杂的、能够快速响应用户输入而不会出现卡顿、延迟或视觉跳动的用户界面。未来,我们将利用这些新功能为 FlatList 和 TextInput 等内置组件带来更多改进。

Reanimated 这样的流行库已经在使用新的渲染器

“Reanimated 4,目前正在开发中,引入了一个新的动画引擎,直接与新的渲染器协同工作,使其能够处理动画并管理跨不同线程的布局。新渲染器的设计真正实现了这些功能的构建,而无需依赖大量的变通方法。此外,因为它是在 C++ 中实现的,并且跨平台共享,Reanimated 的大部分都可以一次编写,减少了特定于平台的问题,最小化了代码库,并简化了树外平台的采用。”

Krzysztof MagieraReanimated 的创建者

事件循环

新架构允许我们实现一个明确定义的事件循环处理模型,如这篇RFC中所述。此 RFC 遵循HTML 标准中描述的规范,它描述了 React Native 应如何在 JavaScript 线程上执行任务。

实现一个明确定义的事件循环弥补了 React DOM 和 React Native 之间的差距:React Native 应用程序的行为现在更接近 React DOM 应用程序的行为,使其更容易一次学习,随处编写。

事件循环为 React Native 带来了许多好处

  • 中断渲染以处理事件和任务的能力
  • 与 Web 规范更紧密对齐
  • 更多浏览器功能的基础

通过事件循环,React 能够可预测地排序更新和事件。这允许 React 用紧急的用户事件中断低优先级更新,并且新的渲染器允许我们独立渲染这些更新。

事件循环还将事件和任务(如计时器)的行为与 Web 规范对齐,这意味着 React Native 的工作方式更像用户在 Web 中所熟悉的,并允许 React DOM 和 React Native 之间更好地共享代码。

它还允许实现更兼容的浏览器功能,如微任务、MutationObserverIntersectionObserver。这些功能尚未准备好在 React Native 中使用,但我们正在努力在未来将它们带给您。

最后,事件循环和新渲染器更改以支持同步读取布局,允许 React Native 为 useLayoutEffect 添加适当的支持,以同步读取布局信息并在同一帧中更新 UI。这允许您在元素显示给用户之前正确放置它们。

有关更多详细信息,请参阅useLayoutEffect

移除桥接

在新架构中,我们还完全移除了 React Native 对桥接的依赖,将其替换为使用 JSI 在 JavaScript 和原生代码之间进行直接、高效的通信

移除桥接通过避免桥接初始化来缩短启动时间。例如,在旧架构中,为了向 JavaScript 提供全局方法,我们需要在启动时在 JavaScript 中初始化一个模块,导致应用程序启动时间略有延迟

// ❌ Slow initialization
import {NativeTimingModule} from 'NativeTimingModule';
global.setTimeout = timer => {
NativeTimingModule.setTimeout(timer);
};

// App.js
setTimeout(() => {}, 100);

在新架构中,我们可以直接从 C++ 绑定方法

// ✅ Initialize directly in C++
runtime.global().setProperty(runtime, "setTimeout", createTimer);
// App.js
setTimeout(() => {}, 100);

重写还改进了错误报告,特别是针对启动时的 JavaScript 崩溃,并减少了因未定义行为导致的崩溃。如果发生崩溃,新的React Native 开发工具可简化调试并支持新架构。

桥接仍然存在,以实现向后兼容性,以支持向新架构的渐进式迁移。未来,我们将完全移除桥接代码。

渐进式迁移

我们预计大多数应用程序升级到 0.76 的工作量与其他版本相同。

当您升级到 0.76 时,新架构和 React 18 默认启用。但是,要使用并发功能并获得新架构的全部好处,您的应用程序和库将需要逐步迁移以完全支持新架构。

当您首次升级时,您的应用程序将在新架构上运行,并带有一个与旧架构自动互操作的层。对于大多数应用程序来说,这将无需任何更改即可工作,但互操作层存在已知限制,因为它不支持访问自定义 Shadow Node 或并发功能。

要使用并发功能,应用程序还需要按照React 规则进行更新,以支持并发 React。要将您的 JavaScript 代码迁移到 React 18 及其语义,请遵循React 18 升级指南

总的策略是在不破坏现有代码的情况下让您的应用程序在新架构上运行。然后,您可以按照自己的节奏逐步迁移您的应用程序。对于所有模块都已迁移到新架构的新界面,您可以立即开始使用并发功能。对于现有界面,您可能需要解决一些问题并迁移模块,然后才能添加并发功能。

我们与最流行的 React Native 库合作,以确保对新架构的支持。超过 850 个库已经兼容,包括所有每周下载量超过 20 万的库(约占下载库的 10%)。您可以在 reactnative.directory 网站上查看库与新架构的兼容性

有关升级的更多详细信息,请参阅下面的如何升级

新特性

新架构全面支持 React 18、并发功能以及 React Native 中的 useLayoutEffect。有关 React 18 功能的完整列表,请参阅React 18 博客文章

过渡

过渡是 React 18 中的一个新概念,用于区分紧急更新和非紧急更新。

  • 紧急更新反映直接交互,例如输入和按下。
  • 过渡更新将 UI 从一个视图过渡到另一个视图。

紧急更新需要立即响应,以符合我们对物理对象行为的直觉。然而,过渡有所不同,因为用户不期望在屏幕上看到每个中间值。在新架构中,React Native 能够支持分别渲染紧急更新和过渡更新。

通常,为了获得最佳用户体验,单个用户输入应同时导致紧急更新和非紧急更新。与 ReactDOM 类似,presschange 等事件被视为紧急事件并立即渲染。您可以在输入事件中使用 startTransition API 来告知 React 哪些更新是“过渡”,可以推迟到后台进行

import {startTransition} from 'react';

// Urgent: Show the slider value
setCount(input);

// Mark any state updates inside as transitions
startTransition(() => {
// Transition: Show the results
setNumberOfTiles(input);
});

将紧急事件与过渡分离可以实现更灵敏的用户界面和更直观的用户体验。

这是旧架构(无过渡)和新架构(有过渡)的比较。想象一下,每个瓦片不是一个具有背景色的普通视图,而是一个包含图像和其他组件的丰富组件,它们渲染成本很高。**在**使用 useTransition 之后,您避免了应用程序因更新而崩溃并落后。

A video demonstrating an app rendering many views (tiles) according to a slider input. The views are rendered in batches as the slider is quickly adjusted from 0 to 1000.
之前:不标记为过渡的瓦片渲染。
A video demonstrating an app rendering many views (tiles) according to a slider input. The views are rendered in batches as the slider is quickly adjusted from 0 to 1000. There are less batch renders in comparison to the next video.
之后:带过渡的瓦片渲染,中断正在进行的过时状态渲染。

有关更多信息,请参阅支持并发渲染器和功能

自动批处理

升级到新架构时,您将受益于 React 18 的自动批处理。

自动批处理允许 React 在渲染时将更多状态更新批处理在一起,以避免渲染中间状态。这使得 React Native 更快,更不容易出现滞后,而无需开发人员编写任何额外的代码。

A video demonstrating an app rendering many views according to a slider input. The slider value is adjusted from 0 to 1000 and the UI slowly catches up to rendering 1000 views.
之前:使用传统渲染器渲染频繁的状态更新。
A video demonstrating an app rendering many views according to a slider input. The slider value is adjusted from 0 to 1000 and the UI resolves to 1000 views faster than the previous example, without as many intermediate states.
之后:使用自动批处理渲染频繁的状态更新。

在旧架构中,会渲染更多的中间状态,即使滑块停止移动,UI 也会不断更新。新架构渲染的中间状态更少,并且由于自动批处理更新,渲染完成得更快。

有关更多信息,请参阅支持并发渲染器和功能

useLayoutEffect

基于事件循环和同步读取布局的能力,在新架构中,我们增加了对 React Native 中 useLayoutEffect 的正确支持。

在旧架构中,您需要使用异步的 onLayout 事件来读取视图的布局信息(这也是异步的)。因此,至少会有一帧的布局不正确,直到布局被读取和更新,导致工具提示放置位置不正确等问题。

// ❌ async onLayout after commit
const onLayout = React.useCallback(event => {
// ❌ async callback to read layout
ref.current?.measureInWindow((x, y, width, height) => {
setPosition({x, y, width, height});
});
}, []);

// ...
<ViewWithTooltip
onLayout={onLayout}
ref={ref}
position={position}
/>;

新架构通过允许在 useLayoutEffect 中同步访问布局信息来解决此问题

// ✅ sync layout effect during commit
useLayoutEffect(() => {
// ✅ sync call to read layout
const rect = ref.current?.getBoundingClientRect();
setPosition(rect);
}, []);

// ...
<ViewWithTooltip ref={ref} position={position} />;

此更改允许您同步读取布局信息并在同一帧中更新 UI,从而使您能够在元素显示给用户之前正确放置它们

A view that is moving to the corners of the viewport and center with a tooltip rendered either above or below it. The tooltip is rendered after a short delay after the view moves
在旧架构中,布局在 onLayout 中异步读取,导致工具提示的位置延迟。
A view that is moving to the corners of the viewport and center with a tooltip rendered either above or below it. The view and tooltip move in unison.
在新架构中,布局可以在 useLayoutEffect 中同步读取,在显示之前更新工具提示位置。

有关更多信息,请参阅同步布局和效果文档。

完全支持 Suspense

Suspense 允许您声明性地指定组件树的某个部分的加载状态,如果它尚未准备好显示

<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>

几年前我们引入了有限版本的 Suspense,React 18 添加了完整支持。到目前为止,React Native 无法支持 Suspense 的并发渲染。

新架构完全支持 React 18 中引入的 Suspense。这意味着您现在可以在 React Native 中使用 Suspense 来处理组件的加载状态,并且在显示加载状态时,暂停的内容将在后台渲染,从而对可见内容的用户输入给予更高的优先级。

更多信息请参阅React 18 中的 Suspense RFC

如何升级

要升级到 0.76,请按照发布文章中的步骤操作。由于此版本也升级到 React 18,您还需要遵循React 18 升级指南

多亏了与旧架构的互操作层,这些步骤对于大多数应用程序升级到新架构来说应该足够了。但是,要充分利用新架构并开始使用并发功能,您需要将自定义原生模块和原生组件迁移以支持新的原生模块和原生组件 API。

如果不迁移自定义原生模块,您将无法获得共享 C++、同步方法调用或 Codegen 的类型安全带来的好处。如果不迁移原生组件,您将无法使用并发功能。我们建议尽快将所有原生组件和原生模块迁移到新架构。

注意

在未来的版本中,我们将移除互操作层,模块将需要支持新架构。

应用

如果您是应用程序开发人员,要完全支持新架构,您需要升级您的库、自定义原生组件和自定义原生模块以完全支持新架构。

我们与最流行的 React Native 库合作,以确保对新架构的支持。您可以在 reactnative.directory 网站上查看库与新架构的兼容性。

如果您应用程序依赖的任何库尚未兼容,您可以

  • 向库提交一个问题,并要求作者迁移到新架构。
  • 如果库不再维护,请考虑使用具有相同功能的替代库。
  • 在这些库迁移期间选择退出新架构

如果您的应用程序有自定义原生模块或自定义原生组件,我们期望它们能正常工作,这得益于我们的互操作层。但是,我们建议将它们升级到新的原生模块和原生组件 API,以完全支持新架构并采用并发功能。

请按照以下指南将您的模块和组件迁移到新架构

如果您是库维护者,请首先测试您的库是否与互操作层一起工作。如果不行,请在新架构工作组上提出问题。

为了完全支持新架构,我们建议您尽快将您的库迁移到新的原生模块和原生组件 API。这将允许您的库用户充分利用新架构并支持并发功能。

您可以按照以下指南将您的模块和组件迁移到新架构

选择退出

如果出于任何原因,新架构在您的应用程序中行为不正常,您总可以选择退出,直到您准备好再次打开它。

要选择退出新架构

  • 在 Android 上,修改 android/gradle.properties 文件并关闭 newArchEnabled 标志
-newArchEnabled=true
+newArchEnabled=false
  • 在 iOS 上,您可以通过运行以下命令重新安装依赖项
RCT_NEW_ARCH_ENABLED=0 bundle exec pod install

致谢

将新架构交付给 OSS 社区是一项巨大的努力,我们为此花费了数年的研究和开发。我们想借此机会感谢 React 团队所有现任和前任成员,他们帮助我们取得了这一成果。

我们也非常感谢所有与我们合作实现这一目标的合作伙伴。特别要感谢

  • Expo,感谢他们早期采用新架构,并支持迁移最流行的库的工作。
  • Software Mansion,感谢他们维护生态系统中关键的库,早期将其迁移到新架构,以及在调查和修复各种问题方面提供的所有帮助。
  • Callstack,感谢他们维护生态系统中的关键库,早期将其迁移到新架构,以及在社区 CLI 工作中提供的支持。
  • Microsoft,感谢他们在 react-native-windowsreact-native-macos 以及其他几个开发工具中添加了新架构实现。
  • ExpensifyKrakenBlueskyBrigad 率先采用新架构并报告了各种问题,以便我们为其他人修复它们。
  • 所有独立库维护者和开发者,他们通过测试新架构、修复一些问题以及提出不清楚的问题,帮助我们澄清这些问题,从而为新架构做出了贡献。