React Native JSI 深入剖析 --- 第 2 部分中文学习笔记:Bridge vs JSI,发生了什么变化以及为什么重要
文章主旨
这篇文章解释了 React Native 旧架构中的 Bridge 为什么会成为性能瓶颈,以及 New Architecture 中的 JSI 如何替代 Bridge。
核心观点可以概括为:
- 旧 Bridge 把 JS 与 native 之间的所有调用都变成"序列化后的异步消息"。
- 每次跨边界调用都要经历 JSON serialization、batch queue、thread hopping、deserialization 等开销。
- JSI 不再传 JSON 消息,而是让 JavaScript runtime 直接持有 C++ function / host object 的引用。
- JSI 支持同步调用、
jsi::Value传值、ArrayBuffer零拷贝访问,从根本上改变了能在 React Native 中构建的 native 能力类型。 - 但 JSI 不是免费的:它带来 C++、线程亲和性、native crash、调试困难等复杂性。
快速回顾 Part 1
Part 1 建立了 React Native 的基础心智模型:
text
JS Thread UI Thread Native Background Threads
┌──────────┐ ┌──────────┐ ┌──────────────────────┐
│ Hermes │ │ UIKit / │ │ Native modules / I/O │
│ JS code │ │ Android │ │ C++ / Java / ObjC │
└──────────┘ └──────────┘ └──────────────────────┘
React Native 不是一个单线程 JavaScript 应用,而是多个执行域之间通过接口通信的系统。Part 2 聚焦一个问题:在 JSI 之前,这些世界之间如何通信?答案就是 Bridge。
旧架构的 Bridge 到底是什么
Bridge 的正式实现核心是 BatchedBridge / MessageQueue.js。在旧架构中,绝大多数 JS ↔ native 调用都会经过它。
举一个旧架构 native module:
java
@ReactMethod
public void multiply(double a, double b, Promise promise) {
promise.resolve(a * b);
}
JS 侧调用:
js
const result = await NativeModules.MathModule.multiply(3, 7);
console.log(result); // 21
看起来只是两个数字相乘,实际乘法只需要纳秒级时间;但旧架构下,这次调用可能需要毫秒级时间。原因不是乘法慢,而是跨 Bridge 的过程慢。
一次 Bridge 调用的流程
旧架构中调用 multiply(3, 7) 大致会经历以下步骤:
text
JavaScript Bridge Native
│ │ │
│ NativeModules.MathModule │ │
│ .multiply(3, 7) │ │
│ │ │
│ 1. 序列化调用 │ │
│ moduleIDs: [42] │ │
│ methodIDs: [3] │ │
│ params: [[3, 7]] │ │
│ │ │
│ 2. 放入 batch queue ───────────▶ │ │
│ │ │
│ │ 3. 等待 batch flush │
│ │ │
│ │ 4. flush batch ─────────────────▶ │
│ │ │
│ │ 5. JSON.parse │
│ │ 6. 找到 module │
│ │ 7. 调用 method │
│ │ 8. 执行 3 * 7 │
│ │ 9. 序列化结果 │
│ │ ◀──────────────── 10. 返回结果 │
│ 11. 反序列化结果 ◀────────────── │ │
│ 12. resolve promise │ │
真正有业务价值的只有第 8 步,其余都是通信开销。
Bridge 的成本 1:JSON 序列化
Bridge 的第一大成本是 JSON serialization。
跨 Bridge 的每个值都要被转换成 JSON,然后在另一侧解析回来:
| 数据类型 | Bridge 中发生的事 |
|---|---|
| number | 转为 JSON number,再解析回来 |
| string | escape、序列化、解析 |
| object | 整棵对象树 JSON.stringify |
| array | 整个数组序列化 |
| binary data | JSON 不支持,通常要 Base64 或临时文件 |
对于 multiply(3, 7) 这种小参数,这只是浪费但还能接受。但如果传的是大数据,例如 10,000 个对象:
js
NativeModules.DataProcessor.process(items);
Bridge 需要:
JSON.stringify10,000 个对象;- 拷贝序列化后的字符串;
- native 侧再
JSON.parse; - native code 才真正开始处理。
因此,Bridge 调用的成本往往和数据大小成正比,而不是和 native 侧实际运算复杂度成正比。
二进制数据尤其痛苦
JSON 没有 typed array、binary data、ArrayBuffer 的概念。如果要把图片像素、音频样本、视频帧传到 native,旧架构常见做法是:
- Base64:体积膨胀约 33%,还要编码/解码;
- 临时文件:写磁盘,再把路径传过去;
- 多次 copy:没有共享内存指针。
这让实时音频、图像处理、ML inference 这类任务很难高效实现。
Bridge 的成本 2:所有调用都是异步
Bridge 的另一个核心限制是:所有 native calls 都是 async-only。
即使 native 侧操作可以马上返回,也必须走 batch queue、等待 flush、跨线程执行,再异步返回到 JS。
例如你想写一个同步 key-value store:
js
// 你想要的形式:像 localStorage 一样同步
const theme = Storage.get('theme');
renderApp(theme);
但在 Bridge 架构下,只能写成:
js
const theme = await NativeModules.Storage.get('theme');
renderApp(theme);
这个 await 不只是语法差异,它意味着:
- 当前 async function 被挂起;
- native 调用被放入 queue;
- native thread 稍后执行;
- 结果未来某个 event loop tick 才回来。
对一个 native 侧只需微秒级的缓存读取来说,Bridge 会额外增加毫秒级 round-trip latency。
Batch 行为带来的隐性问题
Bridge batching 本来是优化:多个 native calls 被合并成一个 batch,比每个调用单独发送更便宜。
js
NativeModules.Analytics.track('screen_view');
NativeModules.Storage.get('user_id');
NativeModules.Logger.info('App mounted');
这些调用不会立即执行,而是被收集进一个 batch,等到下一次 flush 时一起穿过 Bridge。
问题在于:
- batch 内的调用派发顺序通常可以保持;
- 但如果 async methods 被派发到不同 native threads,它们的完成顺序不一定保持;
- 因此
write('key', 'value')后马上read('key'),理论上可能 read 先完成,write 后完成。
这类 race condition 在 Bridge-based native modules 中很常见,而且非常难复现。
Bridge 在哪里崩掉了
Bridge 对简单 App 是够用的,历史上也支撑了大量 RN 应用上线。但它有明显上限,尤其在三类场景中表现糟糕。
1. 高频事件
典型例子是 JS-driven scroll-linked animation。
滚动事件需要穿过 Bridge 到 JS,JS 计算动画值后,再把结果穿过 Bridge 发回 UI/native:
text
UI Thread Bridge JS Thread
│ │ │
│ Scroll offset ─────▶ │ │
│ │ Serialize ────────▶ │
│ │ │ Process event
│ │ │ Compute animation
│ │ ◀──── Serialize │
│ ◀──── Deserialize │ │
│ Apply update │ │
└───────────────────────┴───────────────────────┘
必须在一帧预算内完成:60fps 下约 16ms
每个事件至少涉及双向 serialization / deserialization。滚动速度高时,事件每秒可能触发几十次,开销迅速叠加。
这也是为什么 RN 早期引入了 native animated driver:useNativeDriver: true 会把动画图提前序列化到 native,并在 UI 线程运行,从而绕过每帧过 Bridge 的问题。
2. 大数据传输
图片、音频 buffer、大型数据集跨 Bridge 时,都要序列化完整 payload。无法传共享内存指针。
例如 1MB audio buffer 可能变成 1.33MB Base64 字符串,再经历 copy、传输、解析、decode。原本可以是零成本指针共享的事,被变成了多毫秒 copy 操作。
3. 同步查询
有些操作天然就是同步的:
- 读取缓存值;
- 检查 feature flag;
- 获取高精度 native timestamp;
- 从内存映射存储中读取小字符串。
Bridge 强制它们走异步 round-trip,这也是为什么 react-native-mmkv 这类同步 key-value storage 不可能在旧 Bridge 架构中以同样方式存在。MMKV 的核心价值就是:
js
const value = storage.getString('key'); // 立即返回,无 await
这只能依赖 JSI。
JSI 如何替代 Bridge
JSI(JavaScript Interface)不是"更快的 Bridge",而是完全不同的机制。
Bridge 的模型是:
text
JS object/value → JSON → queue → native parse → native call
JSI 的模型是:
text
JS runtime 持有 C++ function / host object 引用
JS 调用函数 → 直接执行 C++ function pointer
没有 JSON,没有 queue,没有 batch,没有 Bridge。
同样是 multiply(3, 7),JSI 流程更像这样:
text
JavaScript C++ via JSI
│ │
│ multiply(3, 7) │
│ │
│ 1. 调用 C++ function pointer ─────────────▶ │
│ 参数以 jsi::Value 传入 │
│ │ 2. 读取参数
│ │ a = args[0].asNumber()
│ │ b = args[1].asNumber()
│ │ 3. 计算 3 * 7 = 21
│ │ 4. 返回 jsi::Value(21)
│ ◀────────────────────────────────────────── │
│ 5. result = 21 │
核心差异:
- 从 12 步变成约 5 步;
- 没有 JSON serialization;
- 没有 batch queue;
- 没有 Promise 必然开销;
- 本质上是一次 C++ function call。
JSI 的关键机制:Function pointer,不是 Message
注册一个 JSI function,本质上是把一个 C++ lambda / function pointer 安装到 JavaScript runtime 的 global object 上:
cpp
runtime.global().setProperty(
runtime,
"multiply",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "multiply"),
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);
}
)
);
JS 侧调用:
js
const result = multiply(3, 7); // 21,同步返回
注意这里没有:
awaitPromise- callback
- JSON
- batch queue
为什么可以同步?因为 JSI function 在调用它的 JS 线程上执行。它不是把消息发送到另一个 native thread 后等待返回,而是在同一个 event loop tick 中进入 C++ 代码。
这也解释了 Part 1 里强调的规则:
jsi::Runtime只能在 JS 线程访问。
这个限制看似麻烦,但正是它让同步调用成为可能。
jsi::Value:不用序列化的值传递
Bridge 把所有值都变成 JSON;JSI 则以 jsi::Value、jsi::Object、jsi::String 等类型直接表示 JS value。
| JavaScript 类型 | Bridge 旧方式 | JSI 新方式 |
|---|---|---|
number |
JSON number → string → parse | jsi::Value 包装 double |
string |
JSON string escape/parse | jsi::String,engine-native string |
boolean |
JSON true/false |
jsi::Value 包装 bool |
object |
整棵对象树 JSON.stringify |
jsi::Object direct handle |
array |
整个 array stringify | jsi::Array(也是 object) |
ArrayBuffer |
不支持,常用 Base64 绕过 | jsi::ArrayBuffer,可零拷贝访问 raw bytes |
function |
不可传递 | jsi::Function,C++ 可调用 |
最关键的是 ArrayBuffer。JSI 允许 C++ 直接访问 JS ArrayBuffer 的 raw memory:
cpp
auto buffer = args[0].asObject(rt).getArrayBuffer(rt);
uint8_t* data = buffer.data(rt);
size_t length = buffer.size(rt);
for (size_t i = 0; i < length; i++) {
data[i] = processAudioSample(data[i]);
}
这就是为什么实时音频、camera frame processor、ML inference 等能力可以在 RN 中变得可行:二进制数据不再必须被编码成 JSON/Base64。
JSI 不是优化 Bridge,而是消除 Bridge
这篇文章一个重要观点是:
JSI 不只是让 Bridge 更快;它让过去不可能的操作变得可能。
典型新增能力包括:
- zero-copy binary data sharing;
- 同步 native function call;
- JS 和 C++ 之间传递 object / function;
- C++ host object 暴露给 JS;
- native 侧直接访问 JS runtime。
这些都不是 JSON serialization layer 能自然支持的。
Bridgeless Mode:JSI 成为默认路径
从 React Native 0.76 开始,New Architecture 默认启用,Bridgeless Mode 成为默认路径。
时间线可以这样理解:
text
RN ≤ 0.72: Bridge 默认开启,JSI 可选
RN 0.73: New Architecture opt-in,Bridgeless Mode 实验性引入
RN 0.74--0.75: New Architecture opt-in;启用 NA 时 Bridgeless 默认
RN 0.76+: New Architecture 默认:Bridgeless Mode + Fabric + TurboModules
仍有 interop layer 兼容旧模块
需要注意:Bridge 代码并没有马上从 RN codebase 中完全移除。为了迁移期兼容,旧的 @ReactMethod / BatchedBridge 模块仍可通过 interop layer 工作。但这个兼容层不是未来主路径,官方计划最终移除 Bridge 和 interop layer。
JSI 的代价:没有免费的午餐
Bridge 虽然慢,但它有几个实际优点:简单、安全、容易调试。
| 维度 | Bridge | JSI |
|---|---|---|
| 线程安全 | JSON message 可跨线程发送,天然更安全 | jsi::Runtime 只能在 JS 线程访问 |
| 调试 | JSON message 容易 log、拦截、重放 | C++ function call 更难追踪,需要 native debugger |
| 语言门槛 | Java/Kotlin/Swift + @ReactMethod |
直接 JSI 通常需要 C++、ObjC++、JNI wiring |
| 崩溃风险 | 管理内存语言为主,崩溃面较小 | C++ 可能 segfault、use-after-free、undefined behavior |
| 简单性 | 注解式 API,容易上手 | 需要理解 runtime、host function、线程、内存 |
因此:
- Bridge 用性能换简单性;
- JSI 用复杂性换性能与能力。
对于 native 调用很少、数据量很小的普通 App,Bridge 曾经足够好。但如果你需要同步访问、大二进制数据、高频 native 调用,JSI 就变得必要。
重要澄清:"Bridge 死了"不等于"不再需要异步通信"
"Bridge is dead" 容易让人误解。
死掉的是:
- JSON serialization bridge;
- BatchedBridge / MessageQueue 作为默认通信通道;
- 所有调用都必须 async queue 的机制。
没有死掉的是:
- 线程之间仍然需要消息传递;
- heavy work 仍然不应该阻塞 JS 线程;
- 后台线程完成工作后,仍然需要通过
CallInvoker/RuntimeExecutor回到 JS。
JSI 改变的是"当你已经在正确线程上跨越 JS/native 边界时的成本",而不是取消了 React Native 的多线程架构。
性能对比:AsyncStorage vs MMKV
文章用 storage read 举例说明 Bridge 和 JSI 的差异。
Bridge-based storage(类似 AsyncStorage):
js
const value = await NativeModules.Storage.get('user_theme');
大致流程:
- 序列化调用;
- 放入 batch;
- 等待 flush;
- 发到 native thread;
- native 侧反序列化;
- 读取 storage;
- 序列化结果;
- 发回 JS;
- JS 侧反序列化;
- resolve promise。
JSI-based storage(如 react-native-mmkv):
js
const value = storage.getString('user_theme');
大致流程:
- 调用 C++ function pointer;
- 从 memory-mapped storage 读取;
- 返回
jsi::String。
文章引用的 benchmark 显示:
- MMKV 读操作约
0.012ms; - AsyncStorage 读操作约
0.24ms; - 大约 20x 提升;MMKV README 中不同设备/数据下报告约 30x。
不过需要区分两个贡献来源:
- 通信层:JSI 去掉了 Bridge serialization / async overhead;
- 存储引擎:MMKV 使用 mmap + protobuf,和 AsyncStorage 的 SQLite 后端本身也不同。
同步 JSI 的使用边界
不要因为 JSI 支持同步,就把所有事情都做成同步。
经验规则:
| 操作耗时 | 建议 |
|---|---|
| < 1ms | 同步 JSI 通常可以接受 |
| 1--5ms | 谨慎,关注频率和是否在交互路径上 |
| > 5ms | 放后台线程,使用 CallInvoker / Promise 返回 |
| 可能 50ms+ | 绝不能阻塞 JS 线程,否则会冻结交互 |
一个耗时 50ms 的同步 JSI 调用会阻塞整个 JS 线程:没有 touch events、没有 timers、没有 callbacks。
和 Part 1 崩溃日志的关联
Part 1 的 crash trace 中没有 Bridge frame:
- 没有
NativeToJsBridge; - 没有
callNativeModules; - 没有 JSON serialization;
- 看到的是
libaudio.so中的 C++ function、destructor、memory operations。
这说明它是一个 JSI module。Bridge 被完全绕过,所以代码以 native speed 运行,也以 native speed 崩溃。
这就是 JSI 的双面性:
- 能力更强、性能更高;
- 但错误也更接近底层,可能直接 segfault。
当前系统图可以更新为:
text
JS Thread UI Thread Native Thread
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Hermes │ │ Platform │ │ │
│ │ │ │ │ │
│ ── JSI ──────┼───┼───────────────┼──▶│ C++ code │
│ direct C++ │ │ │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
注意:JSI call 本身在 JS 线程执行;图中的箭头表示 native code 访问,
不是一次线程切换。
常见问题
React Native 中的 JSI 是什么?
JSI(JavaScript Interface)是一个 C++ API,允许 native code 直接与 JavaScript runtime 交互。它用同步 C++ function call 和可零拷贝的数据共享,替代了旧的 JSON Bridge。
Bridgeless Mode 是什么?
Bridgeless Mode 从 React Native 0.76 起默认启用,表示 JS-to-native 通信不再以 legacy JSON Bridge 为主路径,而是通过 JSI。迁移期间仍有 interop layer 让旧模块继续工作。
JSI 比 Bridge 快多少?
取决于场景。文章引用的 storage benchmark 中,JSI-based MMKV 读操作大约比 bridge-based AsyncStorage 快 20--30 倍。主要原因是消除了 serialization 和 async round-trip,同时 MMKV 的存储引擎本身也更快。
关键要点
- Bridge 把所有东西都序列化成 JSON。 调用再简单,也要付出
JSON.stringify/JSON.parse成本。数据大小往往比计算复杂度更影响延迟。 - Bridge 只能异步调用。 所有调用都会 batch,并在后续 event loop tick 处理。即使 native 侧微秒级完成,也无法同步返回。
- JSI 用 function pointer 替代 serialization message。 JS runtime 持有 C++ host function / object 引用,调用时直接进入 C++。
- JSI 允许同步调用和 zero-copy binary data。
jsi::Value、jsi::ArrayBuffer等能力让音频、图像、ML 等高性能场景可行。 - Bridgeless Mode 从 RN 0.76 起成为默认。 Bridge 兼容层仍存在,但不再是主路径,未来会被移除。
- JSI 用复杂性换性能。 它要求 C++、线程约束和更严格的内存管理,也带来 native crash 风险。
- 不要滥用同步 JSI。 快速查找适合同步;I/O、重计算、可能超 5ms 的任务应放后台线程。
下一篇预告
Part 3 会面向 JavaScript 开发者讲解写 JSI native module 所需的 C++ 子集,包括:
- stack vs heap;
- RAII;
unique_ptr;shared_ptr;- lambda;
- move semantics。
目标不是学习完整 C++,而是掌握构建 JSI module 时必须理解的内存和生命周期概念。
快速参考:Bridge vs JSI
| 对比项 | Bridge(legacy) | JSI(New Architecture) |
|---|---|---|
| 数据传输 | JSON serialization,涉及 copy | 直接 C++ call,可 zero-copy |
| 同步调用 | 不支持,永远 async | 支持,在 JS 线程同步返回 jsi::Value |
| 二进制数据 | Base64 / 临时文件绕过 | ArrayBuffer 可在 JS/C++ 间共享 |
| 模块语言 | Java/ObjC/Kotlin/Swift + @ReactMethod |
C++ + jsi::Runtime,配合平台 wiring |
| 默认路径 | 旧 RN 架构默认 | RN 0.76+ Bridgeless Mode 默认 |
| 调试体验 | message 可 log | native debugger 更重要 |
| 风险 | 性能瓶颈、异步 race | C++ 崩溃、线程误用、内存问题 |