跳到主要内容

新架构已来

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

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

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

新的架构增加了对现代 React 功能的全面支持,包括 SuspenseTransitions自动批处理useLayoutEffect。新的架构还包括新的 Native ModuleNative Component 系统,让您可以使用类型安全的编码,并直接访问原生接口,而无需桥接。

此次发布是我们自 2018 年以来一直在进行的 React Native 的从头重写的结果,我们已特别注意确保新的架构对大多数应用程序来说是渐进式迁移。2021 年,我们成立了 新的架构工作组,与社区合作,确保整个 React 生态系统的平滑升级体验。

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

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

什么是新架构

新架构是对支撑 React Native 的核心系统进行的全面重写,包括组件的渲染方式、JavaScript 抽象与原生抽象之间的通信方式,以及不同线程之间的任务调度方式。虽然大多数用户不必过多关注这些系统的内部工作原理,但这些更改带来了改进和新功能。

在旧架构中,React Native 通过异步桥接(asynchronous bridge)与原生平台进行通信。要渲染一个组件或调用一个原生函数,React Native 需要将原生函数调用序列化并放入桥接队列中,然后异步处理。这种架构的好处是主线程不会因为渲染更新或处理原生模块函数调用而被阻塞,因为所有工作都在后台线程上完成。

然而,用户期望与应用程序的交互能立即得到响应,以获得类似原生应用的体验。这意味着某些更新需要响应用户输入而同步渲染,可能会中断正在进行的渲染。由于旧架构仅支持异步,因此我们需要对其进行重写,以便同时支持异步和同步更新。

此外,在旧架构中,通过桥接序列化函数调用很快成为一个瓶颈,特别是对于频繁的更新或大型对象。这使得应用程序难以稳定地达到 60+ FPS。还存在同步问题:当 JavaScript 层和原生层失去同步时,无法同步地进行协调,这会导致列表显示空白帧以及由于中间状态渲染而导致的 UI 跳跃等 bug。

最后,由于旧架构使用原生层级来维护 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 完全类型安全,通过强制 null 安全修复了一个长期存在的 NullPointerReference 问题,并且能够同步调用原生模块函数使我们能够用新的原生模块 API 替换自定义 JSI 访问。”

Marc Rousavyreact-native-mmkv 的创建者

新的渲染器

我们还对原生渲染器进行了彻底重写,增加了一些好处:

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

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

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

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

这是我们 多平台愿景的重要一步。例如,视图扁平化(View Flattening)是为了避免深层布局树而推出的 Android 独有优化。新的渲染器,拥有共享的 C++ 核心,将此功能带到了 iOS。此优化是自动的,无需设置,只需共享渲染器即可免费获得。

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

Reanimated 这样的流行库已经利用了新的渲染器。

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

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 DevTools 可以简化调试并支持新架构。

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

渐进式迁移

我们预计大多数应用升级到 0.76 所需的精力与其他版本发布升级大致相同。

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

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

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

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

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

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

新功能

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

过渡(Transitions)

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

  • 紧急更新反映直接交互,如键入和点击。
  • Transition 更新将 UI 从一个视图过渡到另一个视图。

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

通常,为了获得最佳用户体验,一次用户输入应该同时产生一次紧急更新和一次非紧急更新。与 ReactDOM 类似,presschange 等事件被视为紧急事件并立即渲染。您可以在输入事件中使用 startTransition API 来告知 React 哪些更新是“Transitions”,可以推迟到后台处理。

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);
});

将紧急事件与 Transitions 分离,可以实现更具响应性的用户界面和更直观的用户体验。

以下是旧架构(无 Transitions)与新架构(有 Transitions)的比较。想象一下,每个图块不仅仅是一个简单的带有背景色的视图,而是一个包含图像和其他渲染成本高的组件的丰富组件。在使用 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.
之前:渲染图块而不将其标记为 Transition。
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.
之后:渲染图块带 Transitions 以中断旧状态的渲染。

有关更多信息,请参阅 Concurrent Renderer 和 Features 的支持

自动批处理(Automatic Batching)

当升级到新架构时,您将受益于 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 也在不断更新。新架构渲染的中间状态更少,并且由于自动批处理更新,渲染速度大大提前。

有关更多信息,请参阅 Concurrent Renderer 和 Features 的支持

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 来处理组件的加载状态,并且在显示加载状态时,悬挂(suspended)的内容将在后台渲染,从而为可见内容的用户输入提供更高的优先级。

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

如何升级

要升级到 0.76,请遵循 发布帖子中的步骤。由于此版本还升级到 React 18,因此您还需要遵循 React 18 升级指南

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

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

注意

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

应用

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

我们已经与最受欢迎的 React Native 库合作,以确保它们支持新架构。您可以在 reactnative.directory 网站上查看库与新架构的兼容性。

如果您的应用程序依赖的任何库尚不支持,您可以

  • 向该库的作者开具 issue 并要求其迁移到新架构。
  • 如果该库未得到维护,请考虑使用具有相同功能的替代库。
  • 在这些库迁移期间,选择退出新架构

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

请遵循这些指南将您的模块和组件迁移到新架构。

如果您是库维护者,请首先测试您的库是否与互操作层兼容。如果不兼容,请在 新架构工作组上开具 issue。

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

您可以遵循这些指南将您的模块和组件迁移到新架构。

选择退出

如果出于任何原因,新架构在您的应用程序中无法正常运行,您始终可以选择在准备好再次启用它之前退出新架构。

要退出新架构:

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

致谢

将新架构交付给开源社区是一项巨大的工程,耗费了我们数年时间的研究和开发。我们想借此机会感谢所有为我们取得这一成果的 React 团队的现任和前任成员。

我们也非常感谢所有与我们合作促成此事的所有合作伙伴。特别是,我们要点名表扬:

  • Expo,早期采用新架构,并支持迁移最受欢迎的库的工作。
  • Software Mansion,维护着生态系统中至关重要的库,早期将它们迁移到新架构,并在调查和修复各种问题方面提供了大力帮助。
  • Callstack,维护着生态系统中至关重要的库,早期将它们迁移到新架构,并支持了 Community CLI 的工作。
  • Microsoft,为 react-native-windowsreact-native-macos 以及其他一些开发者工具添加了新架构实现。
  • ExpensifyKrakenBlueskyBrigad,他们率先采用了新架构并报告了各种问题,以便我们能为所有人修复。
  • 所有通过测试、修复一些问题、以及就未明确的事项提出疑问,以便我们能够澄清的独立库维护者和开发者。