React Native JSI 深潜(第二部分):React Native Bridge vs JSI——到底变了什么、为什么变
- 原文:React Native JSI Deep Dive — Part 2: React Native Bridge vs JSI — What Changed and Why
- 作者:Rahul Garg
- 原文发布:2026 年 3 月 18 日
“效率圣杯很容易导向滥用。程序员会在非关键路径的速度上浪费大量时间。大约 97% 的场景里,我们应当忘记那些小优化:过早优化是万恶之源。” — Donald Knuth,Structured Programming with go to Statements,1974
摘要: 在旧架构中,React Native 的每一次 Native Module 调用都要经过同一个瓶颈:Bridge。它会把值序列化成 JSON、把调用塞进异步批处理队列,并让“16ms 内响应”的能力几乎不可实现。JSI 用一种看起来很简单但影响深远的机制替代了它:直接 C++ 函数指针。没有序列化、没有队列、没有 Bridge。本文会沿着同一个 Native 调用在两套架构里各走一遍,让你看到变化的本质。
系列:React Native JSI 深潜(12 篇)
Part 1: React Native Architecture — Threads, Hermes, and the Event Loop | Part 2: React Native Bridge vs JSI — What Changed and Why(你正在阅读) | Part 3: C++ for JavaScript Developers | Part 4: Your First React Native JSI Function | Part 5: HostObjects — Exposing C++ Classes to JavaScript | Part 6: Memory Ownership | Part 7: Platform Wiring | Part 8: Threading & Async | Part 9: Audio Pipeline | Part 10: Storage Engine | Part 11: Module Approaches | Part 12: Debugging
快速回顾
第一部分我们建立了一个基础模型:React Native 的运行由三个执行域构成——JS 线程(Hermes)、UI 线程(平台主线程)和 Native 后台线程——它们主要通过消息传递协作。JS 引擎对外暴露了 jsi::Runtime 这个 C++ 接口。并且留下了一个问题:在 JSI 之前,JS 与 Native 之间的通信到底怎么做?
答案就是 Bridge(旧桥接层)。
Bridge 的性能问题:序列化开销
先看一个旧架构中很普通的 Native Module:
@ReactMethod
public void multiply(double a, double b, Promise promise) {
promise.resolve(a * b);
}
JS 调用:
const result = await NativeModules.MathModule.multiply(3, 7);
console.log(result); // 21
输入两个数,输出一个数。计算本身是纳秒级;但旧架构里,这样一次调用通常要到毫秒级,慢了几个数量级。问题不在“算”,而在“过桥”。
Bridge 的真实工作路径
Bridge(核心是 BatchedBridge + MessageQueue.js)在 JS 与 Native 之间充当总闸门。大多数 JS ↔ Native 调用都要经过它。
调用 multiply(3, 7) 时,大致发生:
- JS 侧把调用编码到批处理数组:
moduleIDs、methodIDs、params - 入队等待 flush
- Bridge 按批次发给 Native
- Native 反序列化并查找 module/method
- 执行真正计算:
3 * 7 = 21 - 结果再序列化回 JS
- JS 反序列化并 resolve Promise
真正业务只有一步,其他都是管理成本。
成本 1:JSON 序列化/反序列化
旧 Bridge 下,跨边界值基本都要做 JSON 编解码。数字、字符串、对象、数组都不例外。
这意味着延迟与“数据体量”强相关,而不是“业务复杂度”。
例如传 10,000 条对象去 Native:
NativeModules.DataProcessor.process(items);
// JS: stringify
// 过桥: 字符串复制
// Native: parse
// 还没开始业务处理,时间已经消耗不少
并且 JSON 对二进制类型并不友好:ArrayBuffer、音频采样、图像像素都要绕路(Base64 或临时文件),额外增大体积和拷贝次数。
关键点: Bridge 让“本来便宜的操作”变贵,让“大数据传输”变得非常痛苦。
为什么 Bridge 调用几乎总是异步
旧 Bridge 是异步批处理模型。即使 Native 侧操作本身是微秒级,也要走“入队 -> flush -> 回调”的完整往返。
你想写:
const theme = Storage.get('theme');
renderApp(theme);
实际常变成:
const theme = await NativeModules.Storage.get('theme');
renderApp(theme);
await 带来的不仅是语法差异,更是调度语义变化:中断当前执行,等待未来 event loop 周期。
批处理还带来一个常见坑:
同批调用的“派发顺序”可以稳定,但分发到不同线程后的“完成顺序”不保证。
例如先 write 后 read,在特定条件下可能先读到旧值。
Bridge 在哪类场景最容易崩盘
1) 高频事件
滚动驱动动画如果要经过 JS,每帧都可能出现双向序列化,帧预算迅速被吃光,容易抖动。
2) 大数据传输
图像、音频、ML 输入数据跨桥时,编码、复制、解码成本很高。
3) 同步查询诉求
缓存读取、feature flag、高精度计时等本应同步返回;Bridge 迫使异步化,给微秒级操作硬塞毫秒级延迟。
这也是为什么像 MMKV 这类“同步 KV 读取”的体验,在旧桥时代很难成立。
JSI:不是优化 Bridge,而是替换 Bridge
JSI 的核心变化不是“把桥修快一点”,而是把通信模型改成“JS 直接持有 C++ host function / host object 引用”。
- 无 JSON 序列化
- 无批处理队列
- 无经典 Bridge
同样的 multiply(3, 7),变成:
- JS 调用 host function
- C++ 直接读取
jsi::Value参数 - 原地计算
- 直接返回
jsi::Value
调用链更短,跨层成本显著下降。
JSI 如何实现:函数指针而非消息
注册一个 JSI 函数,本质上是把 C++ 回调挂进 JS runtime:
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 {
return jsi::Value(args[0].asNumber() * args[1].asNumber());
}
)
);
JS 侧:
const result = multiply(3, 7); // 同步返回
为什么能同步?因为调用发生在 JS 线程上下文内,不再强制跨线程“发消息再回来”。
JSI 值模型:不再先变字符串
Bridge 的主路径是 JSON 字符串;JSI 用 jsi::Value / jsi::Object / jsi::Function / jsi::ArrayBuffer 等类型直接表达值。
特别关键的是 ArrayBuffer:可以零拷贝共享二进制内存(在正确约束下),这为音频、相机、推理等场景打开了新空间。
auto buffer = args[0].asObject(rt).getArrayBuffer(rt);
uint8_t* data = buffer.data(rt);
size_t len = buffer.size(rt);
Bridgeless Mode:默认路径已切到 JSI
从 RN 0.76 开始,Bridgeless Mode 成为默认路径(配合新架构)。
Bridge 相关代码与互操作层在迁移期仍可见,但主路线已经变成 JSI / TurboModules / Fabric。
取舍:性能换复杂度
| 维度 | Bridge | JSI |
|---|---|---|
| 线程安全门槛 | 消息模型天然隔离 | 必须遵守 jsi::Runtime 线程约束 |
| 可观测性 | JSON 易打印/重放 | C++ 调用链调试更重 |
| 开发语言门槛 | Java/Kotlin/Swift 为主 | 直连 JSI 往往要写 C++ |
| 崩溃形态 | 更多逻辑错误 | 增加 C++ 内存错误风险 |
Bridge 偏“简单但慢”;JSI 偏“高性能但更硬核”。
一个实际对比(存储读取)
Bridge 风格(如 AsyncStorage):
const value = await NativeModules.Storage.get('user_theme');
JSI 风格(如 MMKV):
const value = storage.getString('user_theme');
社区 benchmark 常见结论是 JSI 路线在该类读操作上数量级更快(设备与负载不同,倍率会波动)。
注意:这里不仅是通信机制差异,底层存储引擎实现也会影响结果。
经验法则:
- <1ms 的操作可考虑同步 JSI
5ms 的操作更适合后台线程 +
CallInvoker+ Promise
关键结论
- 旧 Bridge 的主要成本是 JSON 编解码 + 异步批处理调度
- JSI 用直接 host function 调用替代序列化消息
- Bridgeless 已成为新架构默认方向
- JSI 不是“银弹”:你用更高开发复杂度换更低跨层开销
与第一篇崩溃日志的关系
第一篇里的崩溃栈,你会看到大量直接 C++ 栈帧,而不是 Bridge 框架层。
这正是 JSI 世界的两面性:更快、更直达,也更接近底层风险面。
参考资料
- React Native — The New Architecture (Official Documentation)
- JSI Source Code — facebook/react-native (jsi.h API Surface)
- React Native 0.76 — The New Architecture Is Here
- React Native Working Group — Bridgeless Mode Discussion
- react-native-mmkv — JSI-based Synchronous Storage
- mrousavy/StorageBenchmark — MMKV vs AsyncStorage
- React Native — Threading Model (Architecture Docs)
- MessageQueue.js — BatchedBridge Implementation
- Tadeu Zagallo — Bridging in React Native
系列:React Native JSI 深潜(12 篇)
Part 1: React Native Architecture — Threads, Hermes, and the Event Loop | Part 2: React Native Bridge vs JSI — What Changed and Why(你正在阅读) | Part 3: C++ for JavaScript Developers | Part 4: Your First React Native JSI Function | Part 5: HostObjects — Exposing C++ Classes to JavaScript | Part 6: Memory Ownership | Part 7: Platform Wiring | Part 8: Threading & Async | Part 9: Audio Pipeline | Part 10: Storage Engine | Part 11: Module Approaches | Part 12: Debugging