Bridge vs JSI,发生了什么变化以及为什么重要

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 需要:

  1. JSON.stringify 10,000 个对象;
  2. 拷贝序列化后的字符串;
  3. native 侧再 JSON.parse
  4. 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,同步返回

注意这里没有:

  • await
  • Promise
  • callback
  • JSON
  • batch queue

为什么可以同步?因为 JSI function 在调用它的 JS 线程上执行。它不是把消息发送到另一个 native thread 后等待返回,而是在同一个 event loop tick 中进入 C++ 代码。

这也解释了 Part 1 里强调的规则:

jsi::Runtime 只能在 JS 线程访问。

这个限制看似麻烦,但正是它让同步调用成为可能。

jsi::Value:不用序列化的值传递

Bridge 把所有值都变成 JSON;JSI 则以 jsi::Valuejsi::Objectjsi::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');

大致流程:

  1. 序列化调用;
  2. 放入 batch;
  3. 等待 flush;
  4. 发到 native thread;
  5. native 侧反序列化;
  6. 读取 storage;
  7. 序列化结果;
  8. 发回 JS;
  9. JS 侧反序列化;
  10. resolve promise。

JSI-based storage(如 react-native-mmkv):

js 复制代码
const value = storage.getString('user_theme');

大致流程:

  1. 调用 C++ function pointer;
  2. 从 memory-mapped storage 读取;
  3. 返回 jsi::String

文章引用的 benchmark 显示:

  • MMKV 读操作约 0.012ms
  • AsyncStorage 读操作约 0.24ms
  • 大约 20x 提升;MMKV README 中不同设备/数据下报告约 30x。

不过需要区分两个贡献来源:

  1. 通信层:JSI 去掉了 Bridge serialization / async overhead;
  2. 存储引擎: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 的存储引擎本身也更快。

关键要点

  1. Bridge 把所有东西都序列化成 JSON。 调用再简单,也要付出 JSON.stringify / JSON.parse 成本。数据大小往往比计算复杂度更影响延迟。
  2. Bridge 只能异步调用。 所有调用都会 batch,并在后续 event loop tick 处理。即使 native 侧微秒级完成,也无法同步返回。
  3. JSI 用 function pointer 替代 serialization message。 JS runtime 持有 C++ host function / object 引用,调用时直接进入 C++。
  4. JSI 允许同步调用和 zero-copy binary data。 jsi::Valuejsi::ArrayBuffer 等能力让音频、图像、ML 等高性能场景可行。
  5. Bridgeless Mode 从 RN 0.76 起成为默认。 Bridge 兼容层仍存在,但不再是主路径,未来会被移除。
  6. JSI 用复杂性换性能。 它要求 C++、线程约束和更严格的内存管理,也带来 native crash 风险。
  7. 不要滥用同步 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++ 崩溃、线程误用、内存问题

参考资料

相关推荐
小书房1 天前
移动开发跨平台方案之RN/Flutter/KMP/CMP
flutter·react native·react·跨平台·rn·kmp·cmp
浩风祭月4 天前
React 18 并发特性实战:用 useTransition 和 useDeferredValue 优化列表搜索体验
前端·react native
老王以为4 天前
单仓库下的四十模块 —— React Monorepo 工程架构拆解
前端·react native·react.js
墨狂之逸才5 天前
npm/yarn 注册表(Registry)与 .npmrc 配置指南
react native
ImTryCatchException5 天前
React Native 嵌入现有 Android 项目:踩坑记录与解决方案
android·react native·react.js
花椒技术6 天前
复杂直播业务做 RN 跨端,我们最后保留了哪些 Native 边界
react native·react.js·harmonyos
wordbaby8 天前
React Native + RNOH:跨页面数据回传的最佳实践与避坑指南
前端·react native
wordbaby9 天前
React Native + RNOH:一个 `lazyScreen()` 搞定 48 页面启动懒加载
前端·react native
wordbaby10 天前
React Native 压缩上传全链路方案:从架构设计到生产实践
前端·react native