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::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');

大致流程:

  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::Value、jsi::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++ 崩溃、线程误用、内存问题

参考资料

相关推荐
enjoywindstorm2 天前
移动端原生框架对比评测
react native
传奇开心果编程2 天前
【声明式UI开发实用技术学与练】第8课 状态提升与下放
学习·flutter·react native·ui·swiftui·composer
传奇开心果编程3 天前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程4 天前
【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context
学习·flutter·react native·ui·swiftui·android jetpack
500844 天前
React Native for OpenHarmony 实战:三方库 react-native-url-polyfill 的鸿蒙化适配指南
javascript·react native·react.js·性能优化·electron·harmonyos
传奇开心果编程4 天前
【现代声明式UI学与练】第9课 性能优化——渲染优化、列表优化、内存优化、启动优化
学习·flutter·react native·ui·性能优化·swiftui·android jetpack
500844 天前
React Native for OpenHarmony 实战:三方库 react-native-volume-control 的鸿蒙化适配指南
javascript·react native·react.js·electron·harmonyos
500849 天前
React Native for OpenHarmony 实战:三方库 react-native-crypto-js 的鸿蒙化适配指南
javascript·react native·性能优化·electron·harmonyos
500849 天前
React Native for OpenHarmony 实战:三方库 react-native-device-name 的鸿蒙化适配指南
深度学习·react native·react.js·机器学习·harmonyos
500849 天前
React Native for OpenHarmony 实战:三方库 react-native-torch 的鸿蒙化适配指南
javascript·react native·react.js·harmonyos