arrow_back返回文章列表

React Native 新架构:Fabric、TurboModules 与迁移路径

React Native 新架构不是一次“打开开关就获得性能”的升级。它重新安排了 JavaScript、渲染器和原生能力的协作方式,但应用最终能否稳定运行,仍取决于依赖库、原生模块、原生组件以及测试方式是否准备好了。把它当作一次依赖治理和边界梳理,通常比把它当成纯框架升级更稳妥。

本文以 2025 年 5 月前公开的 React Native 资料为边界。React Native 0.76 已让新架构在新项目中默认启用,并保留了互操作层与临时退出路径;这意味着迁移可以分阶段完成,也意味着“能启动”并不等于已经获得 Fabric 与 TurboModules 的全部能力。文中的模块名和数据均为演示。

先看它解决的是什么问题

旧架构依赖异步 Bridge:JavaScript 调用原生方法或更新视图时,需要把数据序列化、排队,再由另一侧处理。这个模型有清晰的隔离性,却不适合高频、大对象传递,也无法让 React 在某些交互中同步协调布局与优先级。

新架构不是把所有异步都改成同步,而是提供了更直接的 JavaScript Interface(JSI)路径、可并发工作的渲染体系以及更明确的事件循环。它通常从四个部分理解:

  • Fabric 是新的渲染器,负责把 React 的组件描述交给原生视图体系,并支持并发渲染所需的协调。
  • TurboModules 是新的原生模块体系,配合 Codegen 用 TypeScript 规格生成跨端接口胶水,模块也可以按需加载。
  • Codegen 是契约工具:它让 JavaScript 与原生端围绕一份受限制、可生成的类型定义协作,而不是各自维护两套容易漂移的声明。
  • 新事件循环与 Bridgeless 相关能力让任务调度更接近现代 React 的并发语义,但不应据此假设每个旧库都已脱离 Bridge。

因此,迁移的收益首先是能力边界更清晰:并发特性、布局读取和原生接口的类型约束有了共同基础;性能收益需要通过实际启动、交互和内存数据验证,不能预先写进结论。

先升级应用,再清点生态

从 0.76 开始,新项目默认启用新架构,已有应用仍应先完成常规版本升级:比较模板差异、处理 Android Gradle 和 iOS CocoaPods 的变化、跑通现有测试。随后把依赖按风险分组,而不是一次性替换所有包:

分组 优先检查的问题 处理方式
纯 JavaScript 库 是否依赖废弃 RN API 升级并跑单元测试
含原生代码的库 是否声明兼容新架构、是否有预编译限制 查版本说明并在双端真机构建
自定义 Native Module 接口是否依赖 Bridge 回调、动态对象或隐式类型 先靠互操作层验证,再规划 TurboModule
自定义 Native Component 是否直接使用旧 UIManager 或原生视图假设 先验证渲染与事件,再迁移 Fabric 组件

React Native Directory 可作为初筛入口,但它不替代自己项目的编译与关键路径测试。同一库的兼容性还取决于 React Native 版本、平台、是否使用特定原生能力。迁移清单至少应列出包版本、Android/iOS 构建结果、启动、导航、长列表、输入、媒体和原生交互结果;这些比“兼容/不兼容”一个标签更可复查。

如果确实遇到阻塞库,0.76 仍允许暂时退出。Android 在 android/gradle.properties 中设定 newArchEnabled=false;iOS 可在安装 Pods 时用 RCT_NEW_ARCH_ENABLED=0。这是为了给升级留出回退窗口,不是长期分叉策略:退出后要记录原因、负责库和复查版本,否则临时开关会变成无人维护的永久状态。

# android/gradle.properties
newArchEnabled=false
# iOS:仅作为无法兼容时的临时回退
RCT_NEW_ARCH_ENABLED=0 bundle exec pod install

TurboModule:先写接口,再写平台实现

原生模块迁移最有价值的一步,是把“JavaScript 可以怎么调用原生”变成显式规格。规格文件不是普通任意 TypeScript:它必须使用 Codegen 支持的类型和命名方式,随后由构建流程生成 Android、iOS 与 C++ 侧需要的绑定代码。下面是一个只读写本地演示值的最小接口;同步 getItem 只是说明能力,实际接口仍要评估线程、I/O 和调用频率。

// specs/NativeLocalStore.ts
import type { TurboModule } from 'react-native'
import { TurboModuleRegistry } from 'react-native'

export interface Spec extends TurboModule {
  getItem(key: string): string | null
  setItem(key: string, value: string): void
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeLocalStore')

不要为了“新架构化”把网络、文件扫描或数据库查询改成同步方法。同步调用会占用调用线程,适合极小、可预测且确实需要立即返回的值;耗时工作仍应设计异步结果与取消边界。getEnforcing 也意味着模块缺失时会尽早报错,开发阶段很有帮助;若模块是可选能力,应改用可空获取并在业务层提供明确的降级,而不是把错误吞掉。

原生侧实现前先写契约测试:空字符串、null、非法 key、并发调用和模块不存在分别会怎样。旧模块能通过互操作层运行,并不代表它自动获得 Codegen 类型安全、懒加载或跨平台 C++ 复用;这些收益来自有意识地迁移接口与实现。

Fabric:把原生视图当作受约束的组件契约

Fabric 的核心不是把每个 View 重写一遍,而是让自定义原生组件的属性、事件与命令可以由 Codegen 统一描述。以一个演示的原生进度条为例,JS 侧规格应只暴露渲染所需的 props 和结构稳定的事件,避免把原生对象、任意 JSON 或平台句柄直接穿过边界。

// specs/NativeProgressBar.ts
import type { ViewProps } from 'react-native'
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'
import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes'

export interface NativeProps extends ViewProps {
  progress: number
  onComplete?: DirectEventHandler<Readonly<{ value: number }>>
}

export default codegenNativeComponent<NativeProps>('NativeProgressBar')

这份规格的真正作用是提前限制变更。新增 prop 是否可空、事件是否需要冒泡、数值范围由谁校验、Android/iOS 是否都能表达,都会在接口评审时暴露,而不是等运行时发现某个平台静默忽略。对于现有组件,先用互操作层确认它能渲染、事件能回调、命令不会丢失;再选择一个边界小、测试充分的组件改为 Codegen,不要从最复杂的地图、编辑器或视频视图开始。

用双轨验证代替“一次切换”

一个可操作的迁移节奏可以分为五步。第一步,冻结一次可工作的旧架构基线,保存双端构建命令和关键流程结果。第二步,升级到目标 React Native 版本,在默认新架构下只修编译、启动和依赖问题。第三步,把新旧架构都跑一遍关键路径,重点看导航切换、输入焦点、列表回收、手势、动画与所有自定义原生边界。第四步,挑一个原生模块或组件迁移到 Codegen,补齐其契约和异常测试。第五步,逐项消除退出开关和不兼容依赖,持续记录回归矩阵。

检查点不应只看首屏。建议至少覆盖:冷启动与热重载、前后台切换、弱网和失败态、长时间滚动、频繁输入、原生事件高频回调、低端设备上的内存警告,以及 Android/iOS 各自的打包产物。涉及同步接口时,还要单独观察主线程阻塞和帧率。没有某个项目的实测数据时,文章或发布说明里应写“待验证”,而不是以框架宣传代替基线。

迁移的完成标准

新架构迁移的完成标准不是仓库里出现了 newArchEnabled=true,而是边界可以被维护:依赖有兼容记录;原生模块和组件有可生成的类型契约;回退开关有明确的删除条件;关键路径能在两端稳定复现;性能判断来自同口径数据。Fabric、TurboModules 和 Codegen 提供了更好的地基,但是否把地基用好,取决于每一个跨 JavaScript 与原生边界的接口是否足够小、足够清楚、足够可测。

参考资料

flare

保持好奇,继续思考。