跳到主要内容

新架构已到来

·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 应用和其他产品中大规模使用。我们成功地将 React Native 和新架构用于我们为 Quest 设备开发的 Facebook 和 Instagram 应用。

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

新的原生模块

新的原生模块系统是关于 JavaScript 和原生平台如何通信的重大重写。它完全用 C++ 编写,从而解锁了许多新功能

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

在新的原生模块系统中,JavaScript 和原生层现在可以通过 JavaScript Interface (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,已经从迁移到新的原生模块中获益匪浅

“新的原生模块极大地简化了 setup、autolinking 和初始化 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。此优化是自动的,无需设置,随共享渲染器免费提供。

通过这些更改,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 DevTools 简化了调试并支持新架构。

桥接保留用于向后兼容,以支持逐步迁移到新架构。未来,我们将完全移除桥接代码。

逐步迁移

我们预计大多数应用可以以与其他版本相同的精力升级到 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

致谢

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

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

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