文章

RN旧架构Bridge原理深度解析

深度剖析 React Native 旧架构中 Bridge 的异步消息队列机制、JSON 序列化开销和性能瓶颈,理解为什么新架构要推翻重来。

RN旧架构Bridge原理深度解析

一句话概括

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 性能特征和架构演进的关键:

  1. 调试 JavaScript - 原生通信问题:为什么某些操作在 RN 中特别慢?
  2. 理解 RN 的性能瓶颈:为什么列表滚动卡顿?为什么动画不流畅?
  3. 读懂新架构的设计选择:为什么 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();
    }
}

流程中发生了这些事:

  1. JS 调用 ToastModule.show('Hello', 1) → 参数被序列化为 JSON
  2. 序列化后的数据被放入 JS 侧的消息队列
  3. 批量消息通过 Bridge 发送到原生侧
  4. 原生侧反序列化 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) // 下一帧才能拿到
})

这意味着:

  1. 无法同步获取原生端的数据(如图片尺寸、字体度量)
  2. 异步返回的延迟导致 UI 动画的「等待帧」
  3. 连续的原生调用顺序不可保证

瓶颈二:单通道

所有原生模块的调用共享同一个 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");
    // ...
}

这将导致:

  1. 参数处理需要额外的样板代码
  2. 类型错误在运行时才暴露(JS 传递了 string 但原生期望 int)
  3. 复杂类型(如 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 ManagerFabric → 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 架构和列表渲染关系的理解。

答案核心

多层因素叠加导致:

  1. Bridge 传输瓶颈:FlatList 每次渲染时,JS 需要将组件的 props(包括文本、样式、位置信息)序列化为 JSON 通过 Bridge 发送到原生侧。对于 100 个列表项,这可能涉及数千个字段的序列化
  2. 三线程协同延迟:JS 线程计算 → Bridge 传输 → Shadow 线程布局 → 主线程渲染。每一步都有延迟,累计起来导致首帧渲染慢
  3. 手势处理在 JS 线程:Android 的 ScrollView 手势在 JS 线程处理(为了支持 JS 侧的事件监听),这意味着 JavaScript 的卡顿直接影响滚动的响应性
  4. 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 到新架构的迁移是渐进式的,需要注意以下几点:

  1. 原生模块迁移ReactMethodTurboModule,需要将 @ReactMethod 注解改为 Codegen 生成的接口
  2. 线程模型变化:旧架构中,原生模块可能在任意线程被调用;新架构中,TurboModules 默认在主线程运行,需要显式声明后台线程
  3. 事件处理NativeEventEmitterEventEmitter,新架构的事件是同步的
  4. 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 的核心限制总结

  1. 所有跨线程通信必须走 JSON 序列化/反序列化
  2. 所有原生模块共享一个通道,无法优先级控制
  3. 异步模型导致「发送后遗忘」,无法同步获取结果
  4. 类型在传输中丢失,只能通过约定保证正确

新架构的改进方向

  1. JSI:用 C++ 接口引用替换 JSON 序列化
  2. Fabric:简化渲染管道,减少线程切换
  3. TurboModules:按需加载,减少启动开销
  4. Codegen:编译期类型检查,消除运行时类型错误

扩展思考

Bridge 架构的教训并不仅限于 React Native。在前端开发中,很多类似的设计决策都遇到了同样的问题——当跨语言/跨线程通信成为瓶颈时,「序列化复制」的方案总要向「共享内存引用」的方案演进。Electron 中 Context Bridge 的性能局限、Web Worker 中 postMessage 的结构化克隆开销,本质上都是同样的问题。

对于仍在维护旧架构项目的开发者,理解 Bridge 的原理意味着你能精确定位性能瓶颈,而不是盲目地「尝试各种优化」。对于已经迁移到新架构的项目,理解 Bridge 的局限性能帮助你更好地理解新架构的设计价值。

本文由作者按照 CC BY 4.0 进行授权