新架构已上线
React Native 0.76 现已在 npm 上发布,并默认使用新架构!
在 0.76 版本发布博客文章 中,我们分享了此版本中包含的一些重大更改。在本篇文章中,我们将概述新架构及其如何塑造 React Native 的未来。
新架构全面支持现代 React 功能,包括 Suspense、Transitions、自动批处理 和 useLayoutEffect
。新架构还包括新的 原生模块 和 原生组件 系统,使您可以编写类型安全的代码,并直接访问原生接口,而无需桥接。
此版本是我们自 2018 年以来一直在进行的 React Native 的重写,并且我们格外小心地使新架构对大多数应用来说成为渐进式迁移。在 2021 年,我们创建了 新架构工作组,与社区合作,确保整个 React 生态系统都能获得流畅的升级体验。
大多数应用都能够像任何其他版本一样,以相同的工作量采用 React Native 0.76。最受欢迎的 React Native 库已经支持新架构。新架构还包括一个自动互操作性层,以支持与针对旧架构的库进行向后兼容。
在过去几年的开发过程中,我们的团队公开分享了我们对新架构的愿景。如果您错过了其中任何一次演讲,请在此处查看
- 2019 年 React Native EU - 新的 React Native
- 2021 年 React Conf - React 18 主题演讲
- 2022 年 App.js - 将新的 React Native 架构引入开源社区
- 2024 年 React Conf - 第 2 天主题演讲
什么是新架构
新架构是对支撑 React Native 的主要系统的完整重写,包括组件如何渲染、JavaScript 抽象如何与原生抽象通信以及如何在不同线程之间调度工作。尽管大多数用户不必考虑这些系统的工作原理,但这些更改带来了改进和新功能。
在旧架构中,React Native 使用异步桥与原生平台通信。为了渲染组件或调用原生函数,React Native 需要序列化并将原生函数调用与桥一起排队,这些调用将异步处理。这种架构的优点是主线程永远不会被渲染更新或处理原生模块函数调用阻塞,因为所有工作都在后台线程上完成。
但是,用户期望对交互立即反馈,以感觉像原生应用。这意味着某些更新需要同步响应用户输入进行渲染,这可能会中断任何正在进行的渲染。由于旧架构仅为异步,因此我们需要对其进行重写以允许异步和同步更新。
此外,在旧架构中,通过桥序列化函数调用很快成为瓶颈,尤其是在频繁更新或大型对象的情况下。这使得应用难以可靠地实现 60+ FPS。也存在同步问题:当 JavaScript 和原生层不同步时,无法同步协调它们,从而导致错误,例如列表显示空白帧以及由于中间状态渲染导致的视觉 UI 跳动。
最后,由于旧架构使用原生层次结构保留了 UI 的单个副本,并就地修改该副本,因此布局只能在单个线程上计算。这使得无法处理紧急更新(如用户输入),并且无法同步读取布局,例如在布局效果中读取以更新工具提示的位置。
所有这些问题都意味着无法正确支持 React 的并发功能。为了解决这些问题,新架构包含四个主要部分
- 新的原生模块系统
- 新的渲染器
- 事件循环
- 移除桥接
新的模块系统允许 React Native 渲染器同步访问原生层,这使它能够异步和同步处理事件、调度更新以及读取布局。新的原生模块默认情况下也延迟加载,从而为应用带来显著的性能提升。
新的渲染器可以处理跨多个线程的多个正在进行的树,这允许 React 处理多个并发更新优先级,无论是在主线程还是后台线程上。它还支持同步或异步从多个线程读取布局,以支持更具响应性的 UI,而不会出现卡顿。
新的事件循环可以按照明确定义的顺序处理 JavaScript 线程上的任务。这允许 React 中断渲染以处理事件,以便紧急用户事件可以优先于较低优先级的 UI 过渡。事件循环也与 Web 规范保持一致,因此我们可以支持浏览器功能,如微任务、MutationObserver
和 IntersectionObserver
。
最后,移除桥接可以加快启动速度,并在 JavaScript 和原生运行时之间实现直接通信,从而最大程度地降低切换工作的成本。这也有助于改进错误报告、调试并减少由未定义行为导致的崩溃。
新架构现已准备好用于生产环境。Meta 在 Facebook 应用和其他产品中已经大规模使用了它。我们在为我们的 Quest 设备 开发的 Facebook 和 Instagram 应用中成功使用了 React Native 和新架构。
我们的合作伙伴已经开始在生产环境中使用新架构几个月了:请查看 Expensify 和 Kraken 的这些成功案例,并试用 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 Rousavy,
react-native-mmkv
的创建者
新渲染器
我们还完全重写了原生渲染器,增加了几个好处。
- 更新可以在不同线程上以不同的优先级渲染。
- 布局可以同步并在不同线程之间读取。
- 渲染器是用 C++ 编写的,并在所有平台上共享。
更新后的原生渲染器现在将视图层次结构存储在一个不可变的树结构中。这意味着 UI 以无法直接更改的方式存储,允许对更新进行线程安全处理。这使得它能够处理多个正在进行的树,每个树都表示用户界面的不同版本。因此,更新可以在后台渲染而不会阻塞 UI(例如在过渡期间)或在主线程上渲染(响应用户输入)。
通过支持多线程,React 可以中断低优先级更新以渲染紧急更新(例如由用户输入生成的更新),然后根据需要恢复低优先级更新。新渲染器还可以同步并在不同线程之间读取布局信息。这使得能够为低优先级更新进行后台计算,并在需要时进行同步读取,例如重新定位工具提示。
最后,用 C++ 重写渲染器使其能够在所有平台上共享。这确保了相同的代码在 iOS、Android、Windows、macOS 和任何其他 React Native 支持的平台上运行,提供了一致的渲染功能,而无需为每个平台重新实现。
这是朝着我们的多平台愿景迈出的重要一步。例如,视图扁平化 (View Flattening) 是 Android 独有的优化,用于避免深度布局树。新的渲染器,凭借其共享的 C++ 核心,将此功能带到了 iOS。此优化是自动的,无需设置,它与共享渲染器一起免费提供。
通过这些更改,React Native 现在完全支持并发 React 功能(如 Suspense 和 Transitions),从而更轻松地构建对用户输入快速响应的复杂用户界面,而不会出现卡顿、延迟或视觉跳跃。将来,我们将利用这些新功能为 FlatList 和 TextInput 等内置组件带来更多改进。
像Reanimated这样的流行库已经开始利用新渲染器。
“Reanimated 4(目前正在开发中)引入了一个新的动画引擎,该引擎可直接与新渲染器协同工作,使其能够处理动画并在不同线程之间管理布局。新渲染器的设计是真正使这些功能得以构建而无需依赖大量变通方案的关键。此外,由于它是在 C++ 中实现并在平台之间共享,因此 Reanimated 的大部分内容可以编写一次,从而减少平台特定问题,最小化代码库并简化对外部平台的采用。”
事件循环
新架构使我们能够实现一个定义明确的事件循环处理模型,如本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 之间更好地共享代码。
它还允许实现更多符合浏览器规范的功能,例如微任务、MutationObserver
和 IntersectionObserver
。这些功能目前尚无法在 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 Nodes 或并发功能。
要使用并发功能,应用还需要更新以支持并发 React,方法是遵循React 规则。要将您的 JavaScript 代码迁移到 React 18 及其语义,请遵循React 18 升级指南。
总体策略是在不破坏现有代码的情况下使您的应用在新架构上运行。然后,您可以根据自己的节奏逐步迁移应用。对于已将所有模块迁移到新架构的新界面,您可以立即开始使用并发功能。对于现有界面,您可能需要解决一些问题并在添加并发功能之前迁移模块。
我们已与最受欢迎的 React Native 库合作,以确保它们支持新架构。超过 850 个库已经兼容,包括所有每周下载量超过 200K 的库(约占下载库的 10%)。您可以在reactnative.directory 网站上查看库与新架构的兼容性。
有关升级的更多详细信息,请参阅下面如何升级。
新功能
新架构包括对 React 18、并发功能和 React Native 中的 useLayoutEffect
的完全支持。有关 React 18 功能的完整列表,请参阅React 18 博客文章。
过渡
过渡是 React 18 中的一个新概念,用于区分紧急更新和非紧急更新。
- 紧急更新反映直接交互,例如打字和按键。
- 过渡更新将 UI 从一个视图转换到另一个视图。
紧急更新需要立即响应,以匹配我们对物理对象行为的直觉。但是,过渡有所不同,因为用户并不期望在屏幕上看到每个中间值。在新架构中,React Native 能够分别支持渲染紧急更新和过渡更新。
通常,为了获得最佳的用户体验,单个用户输入应该同时导致紧急更新和非紧急更新。类似于 ReactDOM,press
或 change
等事件被视为紧急事件并立即渲染。您可以在输入事件内部使用 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
后,您避免了应用因更新而出现抖动并落后的情况。
有关更多信息,请参阅 并发渲染器和功能的支持。
自动批处理
升级到新架构后,您将受益于 React 18 的自动批处理。
自动批处理允许 React 在渲染时将更多状态更新组合在一起,以避免渲染中间状态。这使得 React Native 能够更快且更不容易出现延迟,而无需开发人员编写任何额外的代码。
在旧架构中,渲染了更多中间状态,即使滑块停止移动,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,使您能够在元素显示给用户之前正确地定位它们。
有关更多信息,请参阅 同步布局和效果 的文档。
完全支持 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++、同步方法调用或代码生成的类型安全性的好处。如果不迁移原生组件,您将无法使用并发功能。我们建议尽快将所有原生组件和原生模块迁移到新架构。
在未来的版本中,我们将删除互操作层,模块将需要支持新架构。
应用
如果您是应用开发者,要完全支持新架构,您需要升级您的库、自定义原生组件和自定义原生模块以完全支持新架构。
我们已与最受欢迎的 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
致谢
将新架构交付给开源社区是一项巨大的努力,花费了我们数年时间的研究和开发。我们想借此机会感谢所有当前和过去的 React 团队成员,他们帮助我们取得了这一成果。
我们也对所有与我们合作使这一切成为可能的合作伙伴表示衷心的感谢。具体来说,我们要特别感谢
- Expo,感谢他们在早期采用新架构,并支持迁移最受欢迎的库的工作。
- Software Mansion,感谢他们维护生态系统中的关键库,感谢他们在早期将其迁移到新架构,以及在调查和修复各种问题方面提供的所有帮助。
- Callstack,感谢他们维护生态系统中的关键库,感谢他们在早期将其迁移到新架构,以及在社区 CLI 工作中提供的支持。
- Microsoft,感谢他们在
react-native-windows
和react-native-macos
以及其他一些开发者工具中添加了新架构的实现。 - Expensify、Kraken、BlueSky 和 Brigad,感谢他们率先采用新架构并报告各种问题,以便我们能够为所有人修复这些问题。
- 所有独立的库维护者和开发人员,他们通过测试新架构、修复一些问题以及在不清楚的问题上提出疑问来为新架构做出贡献,以便我们能够澄清这些问题。