RN旧架构Bridge原理深度解析
深度剖析 React Native 旧架构中 Bridge 的异步消息队列机制、JSON 序列化开销和性能瓶颈,理解为什么新架构要推翻重来。
一句话概括
React Native 旧架构的 Bridge(桥接)通过异步消息队列实现 JavaScript 与原生线程的通信,但其 JSON 序列化开销和单通道瓶颈成为性能天花板,这正是新架构(JSI/Fabric/TurboModules)全面取代它的根本原因。
背景与意义
React Native(RN)自 2015 年开源以来,一直是跨端开发的重要力量。它的核心卖点是「Learn once, write anywhere」——用 JavaScript 和 React 的思维编写移动应用,同时保留原生级别的性能和体验。
为了实现 JavaScript 和原生平台(iOS/Android)之间的通信,RN 设计了一个名为「Bridge」的架构层。理解 Bridge 的工作原理,是理解 RN 性能特征和架构演进的关键:
- 调试 JavaScript - 原生通信问题:为什么某些操作在 RN 中特别慢?
- 理解 RN 的性能瓶颈:为什么列表滚动卡顿?为什么动画不流畅?
- 读懂新架构的设计选择:为什么 JSI + Fabric + TurboModules 要完全取代 Bridge?
概念与定义
Bridge(桥接):RN 中 JavaScript 执行环境(JSC/Hermes)与原生线程之间的通信通道。它是一个异步、序列化、批处理的消息总线。
JS Thread(JS 线程):运行 JavaScript 代码的线程,负责执行 React 组件渲染逻辑和业务逻辑。
Native Thread(原生线程):包括主线程(UI 渲染)和 Shadow 线程(布局计算)。原生模块的 API 调用在这个线程执行。
消息队列(Message Queue):JS 侧和原生侧各自维护的待处理消息队列。消息在队列中批处理和传输。
Batched Bridge(批量桥接):RN 的 Bridge 不是逐条传递消息,而是将多个消息合并成批次传输,以减少线程间通信次数。
最小示例
Bridge 通信的基本工作流:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// JavaScript 端调用原生模块
import { NativeModules } from 'react-native'
const { ToastModule } = NativeModules
// 这条调用会通过 Bridge 传递给原生
ToastModule.show('Hello from JS', ToastModule.SHORT)
// 原生端(Android)定义的模块
// ToastModule.java
public class ToastModule extends ReactContextBaseJavaModule {
@ReactMethod
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
}
流程中发生了这些事:
- JS 调用
ToastModule.show('Hello', 1)→ 参数被序列化为 JSON - 序列化后的数据被放入 JS 侧的消息队列
- 批量消息通过 Bridge 发送到原生侧
- 原生侧反序列化 JSON → 找到对应的模块和方法 → 执行
核心知识点拆解
1. Bridge 的异步消息队列机制
Bridge 的本质是一个异步、批处理、序列化的消息队列系统。
消息的完整生命周期:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
JS Thread (JavaScript 侧)
│
├── 1. JS 调用原生模块方法
│ → 参数序列化为 JSON(JSON.stringify)
│ → 创建 ModuleMethodCall 对象
│
├── 2. 放入 JS 侧的调用队列
│ → 队列中缓存多个调用
│
├── 3. 定时 Flush(每帧/每次事件循环末尾)
│ → 将队列中的调用打包为 batch
│ → 通过 JSON.stringify 序列化整个 batch
│
├── 4. 通过 Bridge 发送到原生线程
│ → 使用 ReactBridge 的 nativeFlushQueueImmediate
│
└── 原生线程收到后
→ JSON.parse 反序列化
→ 查找 ModuleRegistry
→ 调用匹配的原生方法
JS 侧的调用队列实现(简化):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// MessageQueue.js - RN Bridge 消息队列的简化实现
class MessageQueue {
constructor() {
this._calls = []
this._callID = 0
this._flushing = false
// 注册到定时 flush
this._initFlushLoop()
}
// 调用原生模块
callNativeModule(moduleID, methodID, params) {
const callID = this._callID++
// 将调用存入队列
this._calls.push({
moduleID,
methodID,
params,
callID,
})
// 如果不在 flush 中,立即触发
if (!this._flushing) {
this._flushQueue()
}
return callID
}
// 批量发送队列中的消息
_flushQueue() {
if (this._calls.length === 0) return
if (this._flushing) return
this._flushing = true
// 取出所有待发送的调用
const calls = this._calls.slice()
this._calls = []
// 调用原生侧方法,通过 JNI/ObjC Bridge 传递
// 整个 calls 数组被序列化为一个 JSON 字符串
const json = JSON.stringify(calls)
NativeModules.RCTDeviceEventEmitter
?.__invokeCallback?.(json)
// 或者直接通过 C++ Bridge
this._bridge.callNativeModules(json)
this._flushing = false
}
// 定时 flush(每帧触发)
_initFlushLoop() {
// 在 React 的每一帧结束后干净地 flush
// 实际上是挂接到 JSI 的 __tick 事件上
}
}
原生侧的消息处理(Android - JNI 层简化):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ReactBridge.java - 原生侧处理入站消息
public class ReactBridge {
private final NativeModuleRegistry mRegistry;
private final Queue<Runnable> mNativeModulesQueue = new ConcurrentLinkedQueue<>();
// JS 线程通过 JNI 调用此方法
// messageJSON 是整个 batch 的 JSON 字符串
public void callNativeModules(String messageJSON) {
try {
// 1. 反序列化 JSON
JSONArray calls = new JSONArray(messageJSON);
// 2. 批量处理
for (int i = 0; i < calls.length(); i++) {
JSONObject call = calls.getJSONObject(i);
int moduleID = call.getInt("moduleID");
int methodID = call.getInt("methodID");
JSONArray params = call.getJSONArray("params");
// 3. 查找模块和方法
NativeModule module = mRegistry.getModule(moduleID);
NativeMethod method = module.getMethod(methodID);
// 4. 在主线程执行(或模块指定线程)
mNativeModulesQueue.add(() -> {
method.invoke(params);
});
}
// 5. Flush 到主线程
flushQueueToMainThread();
} catch (JSONException e) {
// 处理解析错误
}
}
}
反向通信(原生 → JS):
原生模块也可以通过 Bridge 给 JS 发送消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Android 原生 → JS 通信
public class MyNativeModule extends ReactContextBaseJavaModule {
@ReactMethod
public void startTimer(int interval) {
new Timer().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// 通过事件发射器发送到 JS
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("onTick", Arguments.createMap());
}
}, 0, interval);
}
}
1
2
3
4
5
6
7
// JS 端接收原生事件
import { NativeEventEmitter, NativeModules } from 'react-native'
const eventEmitter = new NativeEventEmitter(NativeModules.MyNativeModule)
eventEmitter.addListener('onTick', (data) => {
console.log('Tick received:', data)
})
2. JSON 序列化开销分析
JSON 序列化/反序列化是 Bridge 性能的最大瓶颈。每次跨线程调用,参数和返回值都需要经过 JSON.stringify → JSON.parse。
序列化开销的实验数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 序列化开销测试
function benchmarkJSON(size) {
const data = Array.from({ length: size }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random(),
nested: {
x: i * 10,
y: i * 20,
},
tags: ['tag1', 'tag2', 'tag3'],
}))
console.time('stringify')
const str = JSON.stringify(data)
console.timeEnd('stringify')
console.time('parse')
const parsed = JSON.parse(str)
console.timeEnd('parse')
return { stringifiedLength: str.length }
}
// 测试不同数据量的序列化时间
benchmarkJSON(10) // ~0.1ms per operation
benchmarkJSON(100) // ~0.5ms per operation
benchmarkJSON(1000) // ~5ms per operation
一个 1000 条数据的列表,在 RN 中发生重渲染时(比如输入框输入文字导致了 FlatList 刷新),Bridge 需要序列化和传输所有列表项数据。当列表需要频繁更新时,这种序列化开销的累积效应非常显著。
序列化开销的连锁影响:
1
2
3
4
5
6
7
8
9
10
11
JS 端 setState
→ React diff → 确定需要更新的组件
→ 生成 Shadow Tree 更新(布局信息)
→ 序列化 Shadow Tree 更新为 JSON ← 大量对象 → stringify(耗时)
→ 通过 Bridge 传输(线程切换)
→ 原生线程解析 JSON(parse)
→ Yoga 引擎计算布局
→ 主线程渲染 UI
每次 UI 更新 = 至少两次 JSON 序列化/反序列化
(JS → Shadow, Shadow → UI Manager)
网络 Debug 模式下的额外开销:
在 Debug 模式下,Bridge 的通信还多了一层网络开销——JS 代码运行在 Chrome 中,通过 WebSocket 与原生通信:
1
2
3
4
5
6
Chrome DevTools (JS 执行)
│
├── WebSocket (localhost:8081)
│
└── App (原生)
└── Bridge
这就是为什么 Debug 模式下 RN 明显更慢——多了一次网络通信 + JSON 序列化。
3. 性能瓶颈分析
Bridge 架构的性能瓶颈可以归纳为以下四点:
瓶颈一:异步阻塞
Bridge 是异步的——JS 调用原生方法后不会等待返回结果,而是继续执行。这带来了一个问题:
1
2
3
4
5
6
7
// JS 端
const result = NativeModules.MyCalculator.add(1, 2)
// ❌ result 是 undefined!因为是异步的
// 只能通过回调或 Promise 获取结果
NativeModules.MyCalculator.add(1, 2, (result) => {
console.log('Result:', result) // 下一帧才能拿到
})
这意味着:
- 无法同步获取原生端的数据(如图片尺寸、字体度量)
- 异步返回的延迟导致 UI 动画的「等待帧」
- 连续的原生调用顺序不可保证
瓶颈二:单通道
所有原生模块的调用共享同一个 Bridge 通道。想象一条只允许一辆车通行的道路:
1
2
3
4
5
6
7
Bridge Channel (单车道)
│
├── UI 更新 (Shadow Tree → Layout)
├── 原生模块调用 (Toast, Storage, etc.)
├── 原生事件 (传感器、手势、键盘)
├── 图片加载状态
└── 网络请求结果
当手势事件密集触发时,UI 更新被阻塞;当网络请求频繁返回时,动画掉帧。所有通信都在争抢同一条通道。
瓶颈三:类型不安全
Bridge 使用 JSON 作为通用序列化格式,导致类型信息丢失:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// JS 侧发送
NativeModules.UserModule.saveUser({
id: 42,
name: 'Alice',
settings: { theme: 'dark' },
})
// 原生侧接收(Android)
@ReactMethod
public void saveUser(ReadableMap user) {
// 需要手动从 ReadableMap 中提取字段
int id = user.getInt("id");
String name = user.getString("name");
ReadableMap settings = user.getMap("settings");
// ...
}
这将导致:
- 参数处理需要额外的样板代码
- 类型错误在运行时才暴露(JS 传递了 string 但原生期望 int)
- 复杂类型(如 Date)需要自定义序列化
瓶颈四:线程间抖动
密集的 Bridge 通信会导致线程间的频繁切换:
1
2
帧 1: JS 执行 → 序列化 → Bridge → 反序列化 → 原生执行 → 回调序列化 → Bridge → JS 执行
帧 2: (同一帧内重复上述过程)
每次线程切换都有上下文切换开销,序列化/反序列化又增加了额外的 CPU 负担。当 16ms 的帧预算被这些开销占满时,UI 渲染就没有时间了。
实战案例:Bridge 瓶颈导致的性能问题
以下是一个在实际开发中常见的性能问题——FlatList 滚动卡顿的 Bridge 瓶颈分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 问题场景:高性能 FlatList
function ContactsList() {
const [contacts, setContacts] = useState([])
const [searchText, setSearchText] = useState('')
// 输入框每次输入都会触发整个列表的数据重传
const filteredContacts = useMemo(() => {
return contacts.filter((c) =>
c.name.includes(searchText)
)
}, [contacts, searchText])
// 每个联系人组件
const renderItem = useCallback(({ item }) => (
<ContactCard
name={item.name}
avatar={item.avatar}
phone={item.phone}
email={item.email}
company={item.company}
// 注意:每个联系人包含 10+ 字段
/>
), [])
return (
<View>
<SearchInput value={searchText} onChangeText={setSearchText} />
<FlatList
data={filteredContacts} // 可能 500+ 项
renderItem={renderItem}
maxToRenderPerBatch={10}
windowSize={5}
/>
</View>
)
}
// ❌ Bridge 负载分析(每次快速输入):
// 1. searchText 变化 → 组件重渲染
// 2. filteredContacts 变化 → 新的数据数组(500 项)
// 3. FlatList 需要通知原生侧更新视图
// 4. 500 个联系人数据序列化为 JSON → 通过 Bridge 传输
// 5. 原生侧解析 → 更新 ShadowTree
// 6. 如果有头像 URL,图片组件还要额外 Bridge 通信
// ✅ 优化方案:减少 Bridge 传输量
const renderItem = useCallback(({ item }) => (
// 只传输视图所需的最小字段
<ContactCard
id={item.id}
name={item.name}
avatar={item.avatar}
// 不要传递不需要的字段!
/>
), [])
Bridge Profiler——分析 Bridge 负载的工具:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 手动分析 Bridge 调用量
class BridgeMonitor {
constructor() {
this.callCount = 0
this.totalSize = 0
this.lastLogTime = Date.now()
// hook 到 MessageQueue
this.originalCall = MessageQueue.prototype.callNativeModule
MessageQueue.prototype.callNativeModule = (...args) => {
this.callCount++
// 估算大小
this.totalSize += new Blob([JSON.stringify(args)]).size
this.log()
return this.originalCall.apply(this, args)
}
}
log() {
const now = Date.now()
if (now - this.lastLogTime > 1000) {
console.log(`[Bridge] ${this.callCount} calls/sec, ~${(this.totalSize / 1024).toFixed(1)}KB`)
this.callCount = 0
this.totalSize = 0
this.lastLogTime = now
}
}
}
底层原理(含源码分析)
1. Bridge 的 C++ 层实现
RN 的 Bridge 底层使用 C++ 实现,Java(Android)通过 JNI 调用,iOS 通过 ObjC 桥调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ReactCommon/cxxbridge/MessageQueueThread.cpp
// Bridge 背面的 C++ 线程调度实现
class MessageQueueThread {
public:
virtual ~MessageQueueThread() {}
virtual void runOnQueue(std::function<void()>&&) = 0;
virtual void runOnQueueSync(std::function<void()>&&) = 0;
virtual void quitSynchronous() = 0;
};
// JNI 调用流程 (Android)
// JavaScriptCore (JS 线程)
// → JSValue 转 JSON string
// → env->CallVoidMethod(bridge, callNativeModules, jsonStr) // JNI
// → Java 层 JSON.parse
// → 执行 NativeModule 方法
// 其中 JNI 调用本身就有额外开销:
// 1. 数据类型转换(JS String → C++ jstring → Java String)
// 2. JNI 边界检查
// 3. 线程切换后的对象引用管理
2. 相对旧架构与新架构的对比
为了彻底理解 Bridge 的局限性,以下是新旧架构的核心差异:
| 维度 | 旧架构 (Bridge) | 新架构 (JSI + Fabric + TurboModules) |
|---|---|---|
| 通信方式 | 异步 JSON 序列化 | 同步 C++ 引用传递 |
| 类型安全 | 运行时决定 | 编译时决定(Codegen) |
| 线程模型 | 3 线程(JS/Shadow/Main) | 2 线程(JS/Main) |
| 渲染流程 | Shadow Tree → Yoga → UI Manager | Fabric → Yoga → 直接操作 HostView |
| 模块加载 | 全部初始化 | 按需加载(TurboModules) |
| JS 引擎 | JSC(引擎固定) | 可插拔(JSC/Hermes/V8) |
JSI(JavaScript Interface)的核心改进:
1
2
3
4
5
6
7
8
9
10
11
12
13
// JSI 允许 JS 直接持有 C++ 对象的引用
// 而不是通过 JSON 传递数据
// 旧架构:数据通过 Bridge 拷贝
// [JS] "Hello"
// → stringify → "[5,"Hello"]"
// → Bridge → parse → Java String "Hello"
// → 这是一个完整的拷贝过程
// 新架构:数据通过 JSI 引用
// [JS] 持有 C++ HostObject 的引用
// .name → 直接调用 C++ getter
// 没有拷贝,没有序列化
TurboModules 的按需加载:
1
2
3
4
5
6
7
8
9
10
11
// 旧架构:无论是否使用,所有原生模块在启动时初始化
// NativeModules 包含全部模块 → 增加启动时间
// 新架构:TurboModules 按需加载
import { requireNativeModule } from 'react-native'
// 只在需要时才加载原生模块
async function showToast() {
const Toast = await requireNativeModule('Toast')
Toast.show('Hello!')
}
高频面试题解析
Q1: React Native 旧架构中为什么滚动列表的性能不如原生?
考察点:对 Bridge 架构和列表渲染关系的理解。
答案核心:
多层因素叠加导致:
- Bridge 传输瓶颈:FlatList 每次渲染时,JS 需要将组件的 props(包括文本、样式、位置信息)序列化为 JSON 通过 Bridge 发送到原生侧。对于 100 个列表项,这可能涉及数千个字段的序列化
- 三线程协同延迟:JS 线程计算 → Bridge 传输 → Shadow 线程布局 → 主线程渲染。每一步都有延迟,累计起来导致首帧渲染慢
- 手势处理在 JS 线程:Android 的 ScrollView 手势在 JS 线程处理(为了支持 JS 侧的事件监听),这意味着 JavaScript 的卡顿直接影响滚动的响应性
- Debug 模式的 WebSocket 额外开销:JS 代码运行在 Chrome 中,通过 WebSocket 与原生通信,延迟更高
新架构的 Fabric 重新设计了渲染管道:JS 直接操作原生视图(通过 JSI),减少了一次「JS → Bridge → Shadow → UI Manager」的序列化传输。
Q2: JSON 序列化在 Bridge 中为什么是个大问题?它究竟慢在哪里?
考察点:对序列化性能瓶颈的细节理解。
答案核心:
JSON 序列化不仅仅是「慢」,它引发了一系列问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
问题 1:数据复制
JS 侧有一个对象 → stringify 成字符串 → Bridge 传输 → parse 成新对象
每次调用都完整复制数据,没有零拷贝的可能
问题 2:GC 压力
stringify 和 parse 都会创建大量临时字符串和对象
频繁触发 JavaScript 引擎的 GC(垃圾回收)
GC 会暂停 JS 执行(Stop The World)
这就是为什么 RN 滚动时经常「卡一下」——GC 在运行
问题 3:类型信息丢失
JSON 只支持 string/number/boolean/null/object/array
Date → 需要自定义序列化
Map/Set → 需要转换为数组
函数 → 被丢弃
undefined → 被转化为 null
每条数据经过 JSON 都会丢失一部分信息
问题 4:无法增量更新
即使只修改了一个字段,也要传输整个数据对象
旧架构没有「补丁(patch)」机制
实测数据:一个包含 50 个联系人的数组(每个 8 个字段)在 Bridge 端到端传输约需 3-5ms,其中序列化占 40%,反序列化占 40%,Bridge 传输占 20%。
Q3: 有人说 Bridge 已经被 JSI 取代了,但有些旧项目还在用 Bridge,迁移时需要注意什么?
考察点:对架构迁移的工程实践理解。
答案核心:
Bridge 到新架构的迁移是渐进式的,需要注意以下几点:
- 原生模块迁移:
ReactMethod→TurboModule,需要将@ReactMethod注解改为 Codegen 生成的接口 - 线程模型变化:旧架构中,原生模块可能在任意线程被调用;新架构中,TurboModules 默认在主线程运行,需要显式声明后台线程
- 事件处理:
NativeEventEmitter→EventEmitter,新架构的事件是同步的 - Fabric 组件:如果项目使用了自定义原生 UI 组件,需要升级到 Fabric 的 ComponentDescriptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
迁移策略建议:
Phase 1: 升级 RN 到 0.73+
启用新架构的实验性支持
确保所有第三方库兼容
Phase 2: 逐步迁移原生模块
使用 Codegen 生成类型安全的接口
替换 NativeModules 调用为 TurboModules
Phase 3: 启用 Fabric 渲染
替换 UIManager 调用为直接组件引用
测试自定义 UI 组件的兼容性
Phase 4: 移除旧架构依赖
关闭 Bridge 降级
纯新架构模式运行
整体来看,Bridge 到 JSI 的迁移是一个「从序列化通信到共享内存通信」的范式转变,不仅仅是 API 变化,而是整个架构理念的升级。
总结与扩展
React Native 的 Bridge 架构是 2015 年首次设计时的技术选择,在当时是合理且先进的。但随着移动应用对性能要求越来越高,Bridge 的 JSON 序列化开销、异步单通道瓶颈和类型不安全问题逐渐成为不可忽视的性能天花板。
Bridge 的核心限制总结:
- 所有跨线程通信必须走 JSON 序列化/反序列化
- 所有原生模块共享一个通道,无法优先级控制
- 异步模型导致「发送后遗忘」,无法同步获取结果
- 类型在传输中丢失,只能通过约定保证正确
新架构的改进方向:
- JSI:用 C++ 接口引用替换 JSON 序列化
- Fabric:简化渲染管道,减少线程切换
- TurboModules:按需加载,减少启动开销
- Codegen:编译期类型检查,消除运行时类型错误
扩展思考:
Bridge 架构的教训并不仅限于 React Native。在前端开发中,很多类似的设计决策都遇到了同样的问题——当跨语言/跨线程通信成为瓶颈时,「序列化复制」的方案总要向「共享内存引用」的方案演进。Electron 中 Context Bridge 的性能局限、Web Worker 中 postMessage 的结构化克隆开销,本质上都是同样的问题。
对于仍在维护旧架构项目的开发者,理解 Bridge 的原理意味着你能精确定位性能瓶颈,而不是盲目地「尝试各种优化」。对于已经迁移到新架构的项目,理解 Bridge 的局限性能帮助你更好地理解新架构的设计价值。