ReactNative篇 | 新架构Fabric与TurboModule
前言
上一章我们拆解了 RN 老架构的 Bridge 模式,核心结论是:JSON 序列化 + 异步通信是所有性能问题的根源。Bridge 就像一座收费站,所有车辆(消息)都得排队、过检(序列化),高峰期一堵就全完。
那 Meta 团队是怎么解决这个问题的?答案是——干掉 Bridge。
从 RN 0.68 开始,Meta 推出了全新的架构,核心由三大支柱组成:
- JSI(JavaScript Interface):C++ 层的直接调用,取代 JSON Bridge
- Fabric:全新的渲染系统
- TurboModule:全新的 Native Module 系统
如果面试官问你”RN 新架构做了什么改进”,你不能只说”性能更好了”——你需要说清楚每个组件解决了什么问题、怎么解决的。
本章就把这三大支柱以及配套的 Codegen,从原理到实战讲清楚。
诊断自测
Q1:JSI 是什么?它和 Bridge 最本质的区别是什么?
点击查看答案
JSI(JavaScript Interface)是一个用 C++ 编写的轻量级抽象层,允许 JavaScript 代码直接持有 C++ 对象的引用并同步调用其方法,无需经过 JSON 序列化和异步消息队列。与 Bridge 最本质的区别是:Bridge 是异步的、需要序列化的消息传递,而 JSI 是同步的、基于共享内存的直接调用。
Q2:Fabric 和老的渲染系统相比,最关键的改进是什么?
点击查看答案
Fabric 最关键的改进有两点:
- 渲染管线可以同步执行:不再强制异步通过 Bridge,在需要时(如用户交互触发的高优更新)可以在 UI Thread 上同步完成 JS 调用 → 布局计算 → 视图更新的完整流程
- Shadow Tree 使用 C++ 实现并在多个线程间共享:JS Thread、UI Thread 可以直接访问同一份 C++ Shadow Tree,避免了跨线程的 JSON 数据复制
Q3:TurboModule 相比老的 Native Module,解决了哪些问题?
点击查看答案
TurboModule 解决了三个核心问题:
- 按需加载:不再启动时全量注册所有模块,而是首次使用时才加载,加快了启动速度
- 同步调用:通过 JSI 直接调用 Native 方法,不需要异步通过 Bridge
- 类型安全:通过 Codegen 生成类型化的接口代码,JS 和 Native 之间的参数类型在编译期就能检查
一、新架构全景
先从全局看看新架构长什么样,和老架构做个对比。
老架构 vs 新架构
老架构:
┌──────────┐ ┌──────────┐
│ JS Layer │ ←── Bridge ────→ │ Native │
│ (JSC) │ (JSON/Async) │ Layer │
└──────────┘ └──────────┘
新架构:
┌──────────┐ ┌──────────┐
│ JS Layer │ ←── JSI(C++) ──→ │ Native │
│ (Hermes) │ (直接引用/同步) │ Layer │
└──────────┘ └──────────┘
↕ ↕
┌────────────────────────────────────┐
│ Shared C++ Layer │
│ (Fabric Renderer + TurboModule) │
└────────────────────────────────────┘
新架构的核心思路是引入一个 C++ 共享层,让 JS 和 Native 不再需要通过 Bridge 间接通信,而是通过 C++ 层直接交互。
这个变化带来的影响是全方位的:
- 通信方式:从 JSON 异步变成 C++ 直接调用(同步可选)
- 渲染系统:从老的 UIManager 变成 Fabric
- Native Module 系统:从全量注册变成按需加载的 TurboModule
- 类型系统:新增 Codegen,编译期生成类型化接口
二、JSI:新架构的基石
JSI 是整个新架构的基石。如果说老架构的核心是 Bridge,那新架构的核心就是 JSI。
2.1 JSI 是什么?
JSI(JavaScript Interface)是一个用 C++ 编写的轻量级接口层。它做的事情很简单但很关键:
让 JavaScript 代码可以直接持有 C++ 对象的引用,并同步调用这些对象上的方法。
用代码来理解:
// C++ 端:创建一个 HostObject
class MyModule : public jsi::HostObject {
public:
jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override {
if (name.utf8(rt) == "multiply") {
return jsi::Function::createFromHostFunction(
rt, name, 2,
[](jsi::Runtime& rt, const jsi::Value& thisVal,
const jsi::Value* args, size_t count) -> jsi::Value {
double a = args[0].asNumber();
double b = args[1].asNumber();
return jsi::Value(a * b);
});
}
return jsi::Value::undefined();
}
};
// JS 端:直接调用,就像调用普通 JS 对象一样
const result = global.myModule.multiply(3, 4); // 12,同步返回!
注意这里的关键:JS 端调用 multiply 时,不需要序列化参数、不需要异步等待、不需要经过消息队列。它直接调用了 C++ 函数,C++ 函数直接返回结果。
2.2 JSI vs Bridge:本质区别
| 特性 | Bridge | JSI |
|---|---|---|
| 通信方式 | JSON 序列化 + 异步消息队列 | C++ 直接调用 |
| 同步/异步 | 强制异步 | 同步为主,异步可选 |
| 数据传递 | 复制(序列化/反序列化) | 引用(共享内存) |
| JS 引擎耦合 | 与 JSC 紧耦合 | 抽象接口,引擎无关 |
| 类型安全 | 无,运行时才能发现类型错误 | 有,通过 Codegen 编译期检查 |
2.3 JSI 带来的额外好处:引擎无关
老架构中,Bridge 和 JavaScriptCore(JSC)是紧耦合的。想换一个 JS 引擎非常困难。
JSI 定义了一套抽象的 JavaScript 运行时接口,任何 JS 引擎只要实现了这套接口,就能接入 RN。这就是为什么 Hermes 引擎能够相对顺利地替换 JSC——因为它只需要实现 JSI 接口就行了。
JSI 接口(抽象层)
├── Hermes 实现
├── JSC 实现
└── V8 实现(社区方案)
三、Fabric:新的渲染系统
Fabric 是 RN 新架构的渲染引擎,用来替代老架构的 UIManager。
3.1 老渲染系统的问题回顾
在老架构中,渲染流程大致是:
JS Thread: React reconciliation → 生成 UI 指令
↓ Bridge (JSON, 异步)
Shadow Thread: Yoga 布局计算
↓ Bridge (JSON, 异步)
UI Thread: 创建/更新原生视图
每一步之间都有 Bridge 的异步延迟。而且 Shadow Tree 只存在于 Shadow Thread 中,JS Thread 和 UI Thread 无法直接访问。
3.2 Fabric 的核心改进
Fabric 做了三件关键的事:
第一:用 C++ 实现 Shadow Tree,多线程共享
在 Fabric 中,Shadow Tree 是一个不可变(Immutable)的 C++ 数据结构。由于是 C++ 实现的,它可以在 JS Thread、UI Thread、Background Thread 之间直接共享,不需要通过 Bridge 复制数据。
┌─────────────┐
│ C++ Shadow │ ← JS Thread 可以直接读写(通过 JSI)
│ Tree │ ← UI Thread 可以直接读取
│ (Immutable) │ ← Background Thread 可以做布局计算
└─────────────┘
不可变(Immutable)设计保证了线程安全:每次更新不是修改原来的 Tree,而是创建一个新版本。类似 React 的 Immutable State 思路。
第二:支持同步渲染路径
老架构中,所有渲染都是异步的(因为要过 Bridge)。Fabric 支持多优先级渲染:
- 低优先级更新:仍然异步执行,不阻塞 UI 线程(类似老架构)
- 高优先级更新(如用户交互触发):可以同步执行,在当前帧内完成 JS → 布局 → 渲染的完整流程
这和 React 18 的 Concurrent Features 是配合的。在 RN 新架构中,你可以利用 useTransition、useDeferredValue 等 API 来区分更新优先级。
第三:Renderer 和平台解耦
Fabric 的渲染逻辑在 C++ 层实现,与具体的平台(iOS/Android)解耦。平台相关的部分只需要实现一个平台抽象层,告诉 Fabric 如何创建、更新、删除原生视图即可。
这使得 RN 更容易扩展到新平台(比如 VR、TV、Desktop)。
3.3 Fabric 渲染流程
以一次用户交互触发的 UI 更新为例:
1. [JS Thread] 用户事件触发 setState
2. [JS Thread] React reconciliation,生成新的 React Element Tree
3. [C++ Layer] 通过 JSI 同步创建新的 Shadow Tree(Immutable)
4. [C++ Layer] Yoga 布局计算(可在后台线程执行)
5. [C++ Layer] Diff 新旧 Shadow Tree,生成变更列表
6. [UI Thread] 直接从 C++ 层读取变更列表,更新原生视图
和老架构对比,Bridge 的两次异步通信被完全消除了。JS Thread 通过 JSI 直接操作 C++ Shadow Tree,UI Thread 也直接从 C++ 层读取渲染指令。
四、TurboModule:新的 Native Module 系统
TurboModule 是新架构中替代老 Native Module 的方案。
4.1 老 Native Module 的问题
回顾一下老架构 Native Module 的三大问题:
- 启动时全量注册:即使你只用了 3 个模块,所有 100+ 个模块都要在启动时注册
- 异步调用:通过 Bridge 的 JSON 序列化,即使只是读取一个设备信息也是异步的
- 无类型安全:JS 端传什么参数,Native 端收到什么参数,中间没有任何校验
4.2 TurboModule 如何解决
按需加载(Lazy Loading)
// 老架构:启动时就注册好了
import { NativeModules } from 'react-native';
const camera = NativeModules.Camera; // 启动时已经存在
// 新架构:首次访问时才加载
import { TurboModuleRegistry } from 'react-native';
const camera = TurboModuleRegistry.get('Camera'); // 首次调用时才通过 JSI 加载
TurboModule 使用一个懒加载注册表。模块不在启动时全量初始化,而是在 JS 端第一次请求时,才通过 JSI 去初始化对应的 Native 模块。这对启动速度的提升非常显著,特别是大型项目中可能有几十上百个 Native Module。
同步调用
通过 JSI,JS 端可以同步调用 Native 方法并立即获取返回值:
// 老架构:必须异步
const version = await NativeModules.DeviceInfo.getVersion();
// 新架构:可以同步
const version = TurboModuleRegistry.get('DeviceInfo').getVersion(); // 同步返回
当然,不是所有方法都适合同步调用。耗时操作(如网络请求、文件 I/O)仍然应该是异步的。TurboModule 同时支持同步和异步两种模式,开发者可以根据场景选择。
类型安全(配合 Codegen)
这一点我们在下一节详细讲。
4.3 TurboModule 的工作原理
JS 端调用 camera.takePicture(options)
↓ JSI (C++ 直接调用,不经过 Bridge)
C++ TurboModule Wrapper
↓ 调用平台相关实现
Native Camera Module (ObjC / Java)
↓ 返回结果
C++ TurboModule Wrapper
↓ JSI (直接返回给 JS)
JS 端拿到结果
整个过程没有 JSON 序列化。参数通过 JSI 以 C++ 值类型或引用传递,返回值也是直接传回。
五、Codegen:类型代码自动生成
Codegen 是新架构的”基础设施”,为 Fabric 和 TurboModule 生成类型化的 C++ 接口代码。
5.1 为什么需要 Codegen?
在老架构中,JS 和 Native 之间的”契约”全靠开发者手动保持一致。你在 JS 端调用 NativeModules.Camera.takePicture({ quality: 'high' }) 时,如果 Native 端期望 quality 是一个数字而不是字符串,这个错误只能在运行时才发现。
Codegen 的思路是:用一份类型定义(TypeScript/Flow),自动生成 JS 端和 Native 端的接口代码,确保两边的类型完全一致。
5.2 Codegen 的工作流程
1. 开发者编写 TypeScript/Flow 类型定义(Spec 文件)
↓
2. Codegen 在编译期解析类型定义
↓
3. 生成 C++ 接口代码(供 Fabric/TurboModule 使用)
4. 生成 Native 端的接口代码(ObjC/Java)
5. 生成 JS 端的类型声明
一个 TurboModule 的 Spec 文件示例:
// NativeCamera.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
takePicture(options: {
quality: number;
flash: boolean;
}): Promise<{
uri: string;
width: number;
height: number;
}>;
getAvailableCameras(): Array<string>; // 同步方法
}
export default TurboModuleRegistry.getEnforcing<Spec>('Camera');
Codegen 会根据这个 Spec 自动生成:
- C++ 的
NativeCameraSpecJSI.h,定义了 C++ 层的接口 - ObjC/Java 的抽象基类,Native 端实现时必须遵循
- JS 端的类型声明
如果 Native 端的实现与 Spec 不一致(比如参数类型不对、缺少某个方法),编译期就会报错。
5.3 Codegen 的意义
从工程角度看,Codegen 解决了一个长期困扰 RN 开发者的问题:JS 和 Native 之间的接口变更很容易出错。以前修改一个 Native Module 的接口,需要同时修改 JS 端和 Native 端,而且没有工具帮你检查是否改全了。现在只需要修改 Spec 文件,Codegen 自动保证两端同步。
六、新旧架构迁移策略
如果你的项目正在从老架构迁移到新架构,或者面试官问你”怎么迁移”,这部分内容是必须掌握的。
6.1 渐进式迁移
RN 新架构支持新旧架构共存。你不需要一次性把所有代码都迁移到新架构,而是可以渐进式地迁移:
- 新写的模块用 TurboModule / Fabric Component
- 老的模块暂时保留,通过互操作层(Interop Layer) 继续工作
- 逐步将老模块改写为新架构
6.2 迁移步骤
以 TurboModule 为例,从老 Native Module 迁移的典型步骤:
1. 创建 TypeScript Spec 文件(定义模块接口)
2. 运行 Codegen,生成 C++ / ObjC / Java 接口代码
3. 修改 Native 端实现,继承 Codegen 生成的基类
4. 修改 JS 端调用方式(从 NativeModules 改为 TurboModuleRegistry)
5. 测试 + 验证
6.3 迁移中的常见问题
问题一:第三方库不支持新架构
这是目前迁移中最大的障碍。很多流行的第三方 Native Module 库还没有适配新架构。解决方案:
- 检查库的 GitHub 是否有新架构适配的 PR 或分支
- 使用 RN 的 Interop Layer,让老架构模块在新架构中运行
- 考虑 fork 库自行适配
问题二:Hermes 兼容性
新架构推荐使用 Hermes 引擎(部分功能依赖 Hermes)。如果你的项目之前用的是 JSC,迁移时需要注意:
- Hermes 不支持
with语句、不支持某些 Proxy 的边缘用法 - Hermes 的
Date实现和 JSC 有细微差异 - 部分 polyfill 在 Hermes 上行为可能不同
问题三:性能回归
迁移初期可能会出现部分场景性能反而下降。这通常是因为:
- 新旧架构混用时,Interop Layer 本身有开销
- 某些库的新架构实现还不够成熟
- Codegen 生成的代码可能需要针对特定平台优化
6.4 是否应该立即迁移?
这取决于你的项目情况:
| 场景 | 建议 |
|---|---|
| 新项目 | 直接用新架构 |
| 老项目,第三方库已适配 | 建议迁移 |
| 老项目,强依赖未适配的库 | 等待库适配,或先用 Interop Layer |
| 对性能不敏感的工具类 App | 迁移优先级低 |
| 有大量复杂动画/手势的 App | 优先迁移,收益最大 |
常见误区
误区一:“新架构完全去掉了异步通信”
不是。新架构的核心改进是提供了同步通信的能力,但不是说所有通信都变成了同步。耗时的 Native 操作(网络请求、文件 I/O、数据库查询等)仍然应该是异步的,否则会阻塞 JS Thread。JSI 给了你同步调用的选项,但什么时候该同步、什么时候该异步,是需要开发者自己判断的。
误区二:“新架构只是 Bridge 变快了”
新架构不是”更快的 Bridge”,而是一个完全不同的通信范式。Bridge 是消息传递模型(发送消息 → 等待处理 → 接收结果),JSI 是直接调用模型(直接调用函数 → 同步返回结果)。这两种模型的差异远超”快慢”的范畴——它影响了你能实现什么样的交互体验。比如在老架构中根本不可能实现的”JS 同步读取 Native 数据”,在新架构中就很自然。
误区三:“Fabric 就是在 Native 端实现了 Virtual DOM”
Fabric 的 Shadow Tree 和 React 的 Virtual DOM 不是一回事。Virtual DOM 是 React 在 JS 端维护的组件树表示,用于 Diff 算法。Shadow Tree 是 C++ 层的布局树,存储了每个节点的实际布局信息(位置、尺寸)。Fabric 的 Shadow Tree 更接近于浏览器的 Layout Tree / Render Tree,而不是 Virtual DOM。
误区四:“迁移到新架构就能解决所有性能问题”
新架构解决的是架构层面的性能瓶颈(Bridge 延迟、序列化开销、全量注册等)。但业务层面的性能问题(低效的 re-render、冗余的状态更新、复杂的组件嵌套等)仍然需要通过优化 React 代码来解决。新架构是”提高了天花板”,但你的代码质量决定了实际表现。
小结
本章从新架构的三大支柱出发,完整拆解了 JSI、Fabric、TurboModule 的设计原理和核心改进。
核心要点
- JSI:C++ 抽象层,允许 JS 直接持有 C++ 对象引用并同步调用,取代了 Bridge 的 JSON 异步通信
- Fabric:新渲染系统,C++ Immutable Shadow Tree 多线程共享,支持同步/异步多优先级渲染
- TurboModule:新 Native Module 系统,按需加载、同步调用、类型安全
- Codegen:根据 TypeScript Spec 自动生成 C++/ObjC/Java 接口代码,编译期保证类型一致
- 迁移策略:支持新旧架构共存,渐进式迁移
记忆口诀
新架构三支柱:JSI 管通道,Fabric 管渲染,TurboModule 管模块。Codegen 是裁缝,量好尺寸(Spec)再裁衣(生成代码)。
本章思维导图
- JSI(JavaScript Interface)
- C++ 抽象层,取代 Bridge
- JS 直接持有 C++ 对象引用
- 同步调用,无需 JSON 序列化
- 引擎无关(Hermes / JSC / V8)
- Fabric(新渲染系统)
- C++ Immutable Shadow Tree
- 多线程共享(JS Thread / UI Thread / Background)
- 同步渲染路径(高优先级更新)
- 配合 React Concurrent Features
- 平台解耦,易扩展
- TurboModule(新 Native Module 系统)
- 按需加载(Lazy Loading)
- 同步调用(通过 JSI)
- 类型安全(配合 Codegen)
- Codegen(类型代码自动生成)
- 输入:TypeScript / Flow Spec 文件
- 输出:C++ / ObjC / Java 接口代码
- 编译期类型检查
- 迁移策略
- 新旧架构共存(Interop Layer)
- 渐进式迁移
- 注意:第三方库适配、Hermes 兼容性
- 新架构 vs 老架构
- Bridge (JSON异步) → JSI (C++同步)
- UIManager → Fabric
- Native Module (全量注册) → TurboModule (按需加载)
- 无类型安全 → Codegen
练习挑战
第一题 ⭐(基础):概念对比
请用一句话分别概括 JSI、Fabric、TurboModule 各自解决了老架构的什么问题。
点击查看答案
- JSI:解决了 Bridge 的 JSON 序列化 + 异步通信问题——用 C++ 直接调用取代了消息传递。
- Fabric:解决了老渲染系统中 Shadow Tree 无法跨线程共享、渲染强制异步的问题——用 C++ Immutable Shadow Tree + 同步渲染路径取代了老的 UIManager。
- TurboModule:解决了 Native Module 启动时全量注册、调用必须异步、没有类型安全的问题——用按需加载 + JSI 同步调用 + Codegen 类型检查来取代。
第二题 ⭐⭐(进阶):场景分析
在 RN 新架构中,以下场景哪些适合使用同步调用,哪些仍然应该异步?请逐一分析。
- 读取设备的屏幕尺寸
- 发起一个 HTTP 网络请求
- 从 Native 的 SQLite 数据库查询 1000 条记录
- 获取当前系统的暗黑模式状态
- 调用原生的图片压缩模块
点击查看答案
- 屏幕尺寸 → 同步。数据已经在内存中,读取操作耗时极短(微秒级),同步返回即可。
- HTTP 请求 → 异步。网络请求的耗时是毫秒到秒级,如果同步调用会阻塞 JS Thread。
- 查询 1000 条数据库记录 → 异步。数据库 I/O 是耗时操作,而且 1000 条数据的序列化/传递也需要时间。
- 暗黑模式状态 → 同步。这是一个简单的状态读取,数据在内存中,微秒级操作。
- 图片压缩 → 异步。图片压缩是 CPU 密集型操作,可能耗时几百毫秒,必须异步以免阻塞 JS Thread。
判断标准: 如果操作耗时在微秒级、数据已在内存中,用同步;如果涉及 I/O、网络、CPU 密集计算,用异步。
第三题 ⭐⭐⭐(综合):编写 TurboModule Spec
假设你需要创建一个 TurboModule 用于管理用户偏好设置(UserPreferences),支持以下功能:
- 同步获取某个 key 的 string 值
- 异步保存 key-value 对
- 同步获取所有 key 的列表
- 异步清除所有偏好设置
请编写 TypeScript Spec 文件。
点击查看答案
// NativeUserPreferences.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
// 同步方法:读取内存中的值,耗时极短
getString(key: string): string | null;
// 异步方法:写入磁盘,涉及 I/O
setString(key: string, value: string): Promise<void>;
// 同步方法:返回内存中的 key 列表
getAllKeys(): Array<string>;
// 异步方法:清除磁盘数据,涉及 I/O
clear(): Promise<void>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('UserPreferences');
设计思路:
getString和getAllKeys设为同步,因为偏好设置通常会缓存在内存中,读取操作非常快setString和clear设为异步(返回Promise),因为写入和清除操作涉及磁盘 I/O,不应阻塞 JS Thread- 方法命名遵循了 TurboModule 的惯例,接口继承
TurboModule - 使用
TurboModuleRegistry.getEnforcing而不是get,确保模块不存在时直接报错而不是返回 null
自我检测
读完本章后,对照下面的清单检验一下自己的掌握程度:
- 能画出新架构的整体结构图,并标注 JSI、Fabric、TurboModule 各自的位置
- 能用自己的话解释 JSI 是什么,以及它和 Bridge 在通信模型上的本质区别
- 能说出 Fabric 的三个核心改进(C++ Shadow Tree 共享、同步渲染路径、平台解耦),并解释每个改进解决了什么问题
- 能说清楚 TurboModule 的按需加载、同步调用、类型安全三大特性
- 能解释 Codegen 的作用和工作流程:输入什么、输出什么、解决什么问题
- 能判断一个 Native 方法应该设计成同步还是异步,并说出判断标准
- 能说出新旧架构迁移的大致步骤和可能遇到的问题
- 能在面试中用”老架构问题 → 新架构方案”的链路,完整回答”RN 新架构做了什么改进”这类问题
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90