【翻译】React Native JSI 深潜(第二部分):React Native Bridge vs JSI——到底变了什么、为什么变

3 阅读7分钟

React Native JSI 深潜(第二部分):React Native Bridge vs JSI——到底变了什么、为什么变


“效率圣杯很容易导向滥用。程序员会在非关键路径的速度上浪费大量时间。大约 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) 时,大致发生:

  1. JS 侧把调用编码到批处理数组:moduleIDsmethodIDsparams
  2. 入队等待 flush
  3. Bridge 按批次发给 Native
  4. Native 反序列化并查找 module/method
  5. 执行真正计算:3 * 7 = 21
  6. 结果再序列化回 JS
  7. 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 周期。

批处理还带来一个常见坑:
同批调用的“派发顺序”可以稳定,但分发到不同线程后的“完成顺序”不保证。
例如先 writeread,在特定条件下可能先读到旧值。


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),变成:

  1. JS 调用 host function
  2. C++ 直接读取 jsi::Value 参数
  3. 原地计算
  4. 直接返回 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。


取舍:性能换复杂度

维度BridgeJSI
线程安全门槛消息模型天然隔离必须遵守 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 世界的两面性:更快、更直达,也更接近底层风险面。


参考资料

  1. React Native — The New Architecture (Official Documentation)
  2. JSI Source Code — facebook/react-native (jsi.h API Surface)
  3. React Native 0.76 — The New Architecture Is Here
  4. React Native Working Group — Bridgeless Mode Discussion
  5. react-native-mmkv — JSI-based Synchronous Storage
  6. mrousavy/StorageBenchmark — MMKV vs AsyncStorage
  7. React Native — Threading Model (Architecture Docs)
  8. MessageQueue.js — BatchedBridge Implementation
  9. 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