跳到主要内容
预计阅读 36 分钟

ReactNative篇 | 老架构Bridge模式

前言

React Native 从 2015 年开源至今,已经成为跨端开发领域最具影响力的方案之一。但如果你在面试中说”我用过 RN”,面试官大概率会追问一句:

“说说 RN 的架构?Bridge 是怎么工作的?为什么会卡?”

很多人只知道”RN 有一个 Bridge 连接 JS 和 Native”,但再深入就答不上来了。比如:

  • RN 的三线程模型到底是怎么分工的?
  • Bridge 通信为什么要用 JSON 序列化?这带来了什么问题?
  • Native Modules 和 Native Components 有什么区别?
  • 为什么列表快速滑动时会白屏?从架构角度怎么解释?

本章,我们就从 RN 老架构(0.68 之前的经典架构)出发,把 Bridge 模式从线程模型、通信机制、性能瓶颈三个维度拆解清楚。理解了老架构的问题,你才能真正理解新架构(Fabric + TurboModule)为什么要这样设计。


诊断自测

在正式开始之前,先用几道题检验一下你对 RN 老架构的理解。

Q1:RN 中的 JS 代码运行在哪个线程上?UI 渲染又在哪个线程?两者之间是如何通信的?

点击查看答案

JS 代码运行在 JS Thread 上(由 JavaScriptCore 或 Hermes 引擎驱动)。UI 渲染在 UI Thread(也叫 Main Thread)上执行。两者之间通过 Bridge 进行异步通信:JS 端将调用信息序列化为 JSON,通过 Bridge 发送到 Native 端,Native 端反序列化后执行相应操作。

Q2:为什么 RN 应用在快速滑动长列表时,经常出现白屏或者掉帧?从架构角度怎么解释?

点击查看答案

快速滑动时,UI Thread 需要高频地向 JS Thread 发送滚动事件,JS Thread 计算出需要渲染的新 Cell,再通过 Bridge 把渲染指令发回 Native 端。整个过程是异步的,且每次通信都要经过 JSON 序列化/反序列化。当滑动速度超过了这个异步链路的处理能力时,新的 Cell 来不及渲染,就出现了白屏。这本质上是 Bridge 的异步通信延迟导致的。

Q3:Native Modules 和 Native Components 分别是什么?举个例子。

点击查看答案

Native Modules 是暴露给 JS 端调用的原生功能模块,比如相机、蓝牙、文件系统等。它们不涉及 UI 渲染,只提供功能接口。例如 NativeModules.CameraRoll

Native Components(也叫 Native UI Components)是原生 UI 组件的封装,它们在 Native 端渲染真实的原生视图,但通过 JS 端的 React 组件来控制属性和行为。例如 <MapView><WebView> 就是典型的 Native Components。


一、RN 的整体架构概览

在深入 Bridge 之前,我们先建立一个全局认知:RN 应用到底由哪些部分组成?

一个 RN 应用可以分成三层

┌─────────────────────────────────┐
│         JS Layer                │  React 组件、业务逻辑
│    (JavaScript Thread)          │  运行在 JSCore / Hermes
├─────────────────────────────────┤
│         Bridge                  │  JSON 序列化 + 异步消息队列
│    (序列化 / 反序列化)            │
├─────────────────────────────────┤
│         Native Layer            │  原生 UI 渲染、原生模块
│    (UI Thread + Shadow Thread)  │  iOS: UIKit / Android: View
└─────────────────────────────────┘

JS 层负责业务逻辑和 React 组件树的维护,Native 层负责真正的 UI 渲染和原生能力调用。中间的 Bridge 就是两者的”翻译官”。

这个三层架构是理解所有 RN 性能问题的基础。


二、三线程模型

RN 老架构的运行时涉及三个关键线程,面试中经常考。

2.1 JS Thread

JS Thread 是 RN 应用的”大脑”。你写的所有 JavaScript 代码——React 组件、状态管理、业务逻辑、事件处理——都运行在这个线程上。

它由一个 JavaScript 引擎驱动:

  • JavaScriptCore(JSC):iOS 上默认的引擎,也是早期 Android 上的选择
  • Hermes:Meta 专门为 RN 优化的引擎,2019 年推出,启动速度更快,内存占用更低

JS Thread 的核心职责:

  1. 执行 JS Bundle(你的业务代码)
  2. 运行 React 的 reconciliation(Diff 算法)
  3. 计算出 UI 变更指令,通过 Bridge 发送到 Native 端
  4. 处理来自 Native 端的事件(触摸、滚动等)

关键点:JS Thread 是单线程的。 这意味着如果你的业务逻辑计算量很大(比如大量数据处理),会直接阻塞 UI 更新指令的发送,导致界面卡顿。

2.2 UI Thread(Main Thread)

UI Thread 就是操作系统的主线程,也叫 Main Thread。在 iOS 上是 UIKit 的执行线程,在 Android 上是 Activity 的 UI 线程。

它的职责:

  1. 执行真正的原生 UI 渲染(创建、更新、销毁原生视图)
  2. 处理用户手势和触摸事件
  3. 执行原生动画

所有涉及 UI 的操作必须在这个线程上执行,这是 iOS 和 Android 的系统级要求。

2.3 Shadow Thread

Shadow Thread 是很多人不太了解但非常重要的一个线程。它运行的是 Yoga 布局引擎(Meta 开源的跨平台 Flexbox 布局库,用 C++ 实现)。

它的职责:

  1. 接收 JS Thread 发来的布局描述(Flexbox 属性)
  2. 计算每个节点的实际位置和尺寸(left, top, width, height)
  3. 将计算结果发送给 UI Thread 进行真正的渲染

为什么要单独用一个线程做布局计算?因为布局计算可能比较耗时(特别是复杂嵌套布局),如果放在 UI Thread 上,会阻塞用户交互;如果放在 JS Thread 上,又会阻塞业务逻辑执行。所以 RN 把它独立出来了。

三线程协作流程

用一个实际例子来串联三个线程的协作。假设用户点击了一个按钮,触发一个状态更新,导致列表新增一项:

1. [UI Thread]    用户点击按钮 → 生成触摸事件
2. [UI Thread]    将事件序列化为 JSON → 通过 Bridge 发送
3. [JS Thread]    收到事件 → 执行 onPress 回调 → setState
4. [JS Thread]    React reconciliation → 计算出 DOM 变更
5. [JS Thread]    将变更指令序列化为 JSON → 通过 Bridge 发送
6. [Shadow Thread] 收到布局信息 → Yoga 计算布局
7. [Shadow Thread] 将计算结果发送给 UI Thread
8. [UI Thread]    根据布局结果创建/更新原生视图

整个流程涉及多次跨线程通信,每次都要经过 Bridge 的序列化/反序列化。这就是 RN 老架构性能瓶颈的根源。


三、Bridge 的通信机制

Bridge 是老架构的核心,也是老架构最大的痛点。我们来详细看看它是怎么工作的。

3.1 JSON 序列化

JS 和 Native 是两个完全不同的运行环境——一个是 JavaScript 引擎,一个是 iOS/Android 的原生运行时。它们之间没有共享内存,不能直接互相调用函数。

Bridge 的解决方案是:把所有通信数据转换成 JSON 字符串

比如当 JS 端要创建一个原生 View 时,它会生成这样的消息:

{
  "module": "UIManager",
  "method": "createView",
  "args": [
    42,
    "RCTView",
    1,
    { "backgroundColor": "#ff0000", "width": 200, "height": 100 }
  ]
}

这条消息通过 Bridge 发送到 Native 端,Native 端解析 JSON 后,调用 UIManagercreateView 方法,传入解析后的参数。

反方向也一样。当 Native 端产生了一个触摸事件,它会序列化成 JSON 发送给 JS 端。

3.2 异步批量通信

Bridge 的通信是异步的,而且为了减少通信次数,采用了批量处理策略。

具体来说:

  1. JS 端的渲染指令不会立即发送,而是先积累在一个队列
  2. 每隔一个 tick(通常是每帧,约 16ms),将队列中的所有指令打包成一个 JSON 数组一起发送
  3. Native 端收到后,批量执行所有指令

这种批量处理减少了跨线程通信的频率,但也引入了一个问题:延迟。即使 JS 端已经计算好了结果,也要等到下一个 tick 才能发送。在高频交互场景(如滑动、拖拽)中,这个延迟会被放大。

3.3 通信是双向的

Bridge 的通信是双向的:

  • JS → Native:渲染指令(创建视图、更新属性、删除视图)、调用原生模块
  • Native → JS:用户事件(触摸、滚动)、原生模块的回调、系统事件(生命周期、网络状态变化)

但这两个方向的通信都要经过 JSON 序列化,都是异步的。这意味着任何一个简单的交互(比如点击按钮改变文字颜色),都需要至少两次跨 Bridge 通信:事件从 Native 到 JS,渲染指令从 JS 回到 Native。


四、Bridge 的性能瓶颈

理解了 Bridge 的工作方式后,它的性能问题就很好理解了。面试中这是高频考点。

4.1 序列化开销

JSON 序列化/反序列化本身就有 CPU 开销。对于简单的数据,这个开销可以忽略不计。但在某些场景下,数据量会变得很大:

  • 长列表渲染:一次性传输大量 Cell 的创建指令
  • 大量数据传递:比如把一个大的 JSON 数据从 Native 传给 JS
  • 频繁的小更新:虽然每次数据量不大,但积累起来序列化次数很多

更要命的是,JSON 序列化是在单线程上执行的。JS 端的序列化在 JS Thread 上,Native 端的反序列化在 Native 端的线程上。序列化本身会占用这些线程的执行时间。

4.2 异步通信延迟

Bridge 的异步特性在高频交互场景中问题尤为突出。

最典型的场景是手势跟手。比如用户手指拖动一个元素:

1. [UI Thread]  手指移动 → 触发 touchMove 事件
2. [Bridge]     事件序列化 → 异步发送 → 反序列化
3. [JS Thread]  收到事件 → 计算新位置
4. [Bridge]     渲染指令序列化 → 异步发送 → 反序列化
5. [UI Thread]  更新元素位置

每次手指移动都要走完这个完整链路。在 60fps 的要求下,每帧只有 16ms。如果 Bridge 的往返延迟超过了这个预算,界面就会出现跟手延迟——手指已经移到新位置了,元素还在老位置。

4.3 Bridge 拥塞

当 JS Thread 和 Native 端同时大量使用 Bridge 时,消息队列会堆积,形成拥塞。

一个经典的场景:列表快速滑动 + 网络请求回调

用户快速滑动列表时,大量的滚动事件通过 Bridge 涌向 JS Thread,JS Thread 要计算需要渲染和回收的 Cell,再把渲染指令发回去。如果这时候恰好有一个网络请求返回了大量数据,数据也要通过 Bridge 传给 JS Thread。两股流量叠加,Bridge 就可能出现拥塞,导致帧率急剧下降。

4.4 启动时的 Bridge 开销

应用启动时,所有的 Native Modules 都要在 Bridge 上注册,即使你的页面只用到了其中几个。这个”全量注册”的过程也会拖慢启动速度。(新架构的 TurboModule 就是为了解决这个问题。)


五、Native Modules 和 Native Components

理解 Bridge 之后,我们来看构建在 Bridge 之上的两个核心概念。

5.1 Native Modules

Native Modules 是 RN 暴露原生能力给 JS 端的机制。当 JS 需要调用一些纯原生功能时(比如读取手机相册、调用蓝牙、获取设备信息),就需要通过 Native Module。

工作方式:

  1. Native 端编写一个模块类(iOS 用 Objective-C/Swift,Android 用 Java/Kotlin)
  2. 用 RN 提供的宏/注解将方法标记为可导出
  3. 应用启动时,所有标记的模块和方法在 Bridge 上注册
  4. JS 端通过 NativeModules.ModuleName.methodName() 调用
// JS 端调用
import { NativeModules } from 'react-native';

NativeModules.DeviceInfo.getVersion().then(version => {
  console.log('App version:', version);
});

老架构的问题:

  • 所有 Native Modules 在启动时全量注册,即使没用到也要走一遍注册流程
  • 调用是异步的,必须通过 Bridge 的 JSON 序列化
  • 没有类型安全,JS 端传错参数只能在运行时才发现

5.2 Native Components

Native Components(也叫 Native UI Components)允许你把原生的 UI 组件封装成 React 组件在 JS 端使用。

比如 <MapView> 组件,它在 Native 端渲染的是真正的原生地图(iOS 的 MapKit、Android 的 Google Maps),但你在 JS 端可以用 React 的方式来控制它:

<MapView
  region={{ latitude: 39.9, longitude: 116.4, latitudeDelta: 0.1, longitudeDelta: 0.1 }}
  onRegionChange={region => console.log(region)}
/>

工作方式:

  1. Native 端创建一个 ViewManager,定义支持的属性和事件
  2. JS 端通过 requireNativeComponent 包装成 React 组件
  3. React 渲染时,通过 Bridge 发送创建/更新指令给 Native 端
  4. Native 端的 ViewManager 创建对应的原生视图并管理其生命周期

Native Components 的性能瓶颈同样在 Bridge 上:每次属性更新都需要经过 JSON 序列化的异步通信。


六、从架构角度理解”卡顿”

最后,我们把前面的知识串起来,从架构角度分析几个 RN 老架构中最常见的卡顿场景。面试中如果能这样分析,会让面试官觉得你对底层有真正的理解。

场景一:长列表快速滑动白屏

现象: FlatList 快速滑动时,新的 Cell 来不及渲染,出现空白区域。

架构分析:

快速滑动
→ UI Thread 高频产生 scroll 事件
→ 事件通过 Bridge 异步发送给 JS Thread
→ JS Thread 计算哪些 Cell 需要渲染
→ 渲染指令通过 Bridge 异步发回 Native
→ Shadow Thread 计算布局
→ UI Thread 渲染原生视图

整个链路的延迟 > 单帧时间(16ms),导致视图来不及渲染。

缓解方案:

  • 增大 windowSize:预渲染更多屏幕外的内容
  • 使用 getItemLayout:避免动态计算行高
  • 减少 Cell 的复杂度:降低每个 Cell 的渲染指令数量
  • 使用 initialNumToRender:控制首屏渲染数量

场景二:手势动画不跟手

现象: 拖拽元素时,元素位置跟不上手指,有明显延迟。

架构分析: 手指每次移动都要走 Native → Bridge → JS → Bridge → Native 的完整链路。异步通信让每一帧的更新都有延迟。

缓解方案:

  • 使用 Animated API 的 useNativeDriver: true,让动画完全在 Native 端执行,不经过 Bridge
  • 使用 react-native-reanimated,在 UI Thread 上直接处理动画逻辑
  • 使用 react-native-gesture-handler,在 Native 端处理手势

场景三:JS Thread 繁忙导致全局卡顿

现象: 进行大量数据计算时,连简单的按钮点击反馈都变得迟钝。

架构分析: JS Thread 是单线程的。当它忙于计算数据时,来自 Native 的事件在 Bridge 队列中排队等待。用户的点击事件虽然已经被 UI Thread 捕获了,但 JS Thread 来不及处理,导致点击回调延迟执行。

缓解方案:

  • 使用 InteractionManager.runAfterInteractions 延迟执行非紧急计算
  • 将计算密集型工作移到 Native 端(通过 Native Module)
  • 分批处理数据,使用 requestAnimationFramesetTimeout 让出 JS Thread

常见误区

误区一:“RN 是在 WebView 里跑的”

这是一个非常普遍的误解。RN 不是 WebView 方案(那是 Cordova/Ionic 的路线)。RN 的 JS 代码运行在独立的 JavaScript 引擎中(JSC 或 Hermes),渲染的是真正的原生 UI 组件,而不是 HTML/CSS。RN 的 <View> 对应的是 iOS 的 UIView 或 Android 的 android.view.View,不是 <div>

误区二:“Bridge 的性能问题主要是 JSON 序列化太慢”

序列化本身的 CPU 开销确实是一个因素,但异步通信带来的延迟才是更根本的问题。即使把 JSON 换成二进制协议(比如 Protobuf),只要通信仍然是异步的、需要跨线程传递,延迟问题就无法根治。这也是为什么新架构用 JSI 实现了同步调用——直接绕过了 Bridge 的异步机制。

误区三:“只要用 useNativeDriver 就能解决所有动画卡顿”

useNativeDriver: true 只适用于一部分动画属性(opacitytransform 等),对于布局相关的属性(widthheightmarginTop 等)是不支持的。因为布局变化需要重新计算所有受影响元素的位置,这必须经过 Yoga 布局引擎,无法完全在 UI Thread 上完成。想要更灵活的原生动画,需要使用 react-native-reanimated 这样的库。

误区四:“RN 老架构已经过时了,不用了解”

虽然新架构(Fabric + TurboModule)已经推出,但截至目前,大量线上项目仍在使用老架构。而且理解老架构的问题,才能真正理解新架构每一项改进的动机。面试中,“老架构有什么问题 → 新架构怎么解决的”是一个非常经典的考察链路。


小结

本章从 RN 老架构的三线程模型出发,完整拆解了 Bridge 的通信机制和性能瓶颈。

核心要点

  1. 三线程模型:JS Thread(业务逻辑)、UI Thread(原生渲染)、Shadow Thread(Yoga 布局计算),三者通过 Bridge 协作
  2. Bridge 的通信方式:JSON 序列化 + 异步批量传输,双向通信
  3. 性能瓶颈的三个层面:序列化开销、异步延迟、Bridge 拥塞
  4. Native Modules:暴露原生功能给 JS,启动时全量注册,异步调用
  5. Native Components:封装原生 UI 组件为 React 组件,属性更新通过 Bridge
  6. 常见卡顿场景:长列表白屏(异步延迟)、动画不跟手(通信延迟)、JS 繁忙(单线程阻塞)

记忆口诀

RN 老架构三句话:三个线程各管各,一座 Bridge 全靠它,JSON 异步是代价。


本章思维导图

RN 老架构:Bridge 模式
  • 三线程模型
    • JS Thread:业务逻辑、React reconciliation
    • UI Thread:原生 UI 渲染、手势处理
    • Shadow Thread:Yoga 布局计算
  • Bridge 通信机制
    • JSON 序列化 / 反序列化
    • 异步批量传输(每帧打包发送)
    • 双向通信(JS ↔ Native)
  • 性能瓶颈
    • 序列化 CPU 开销
    • 异步通信延迟(跟手问题)
    • Bridge 拥塞(高频事件 + 大数据叠加)
    • 启动时全量注册 Native Modules
  • Native Modules
    • 暴露原生功能给 JS
    • 全量注册、异步调用、无类型安全
  • Native Components
    • 原生 UI 封装为 React 组件
    • 属性更新经过 Bridge
  • 常见卡顿场景
    • 长列表白屏 → 异步链路延迟
    • 动画不跟手 → Bridge 往返延迟
    • JS 繁忙 → 单线程阻塞
  • 缓解手段
    • useNativeDriver(部分动画)
    • react-native-reanimated / gesture-handler
    • InteractionManager
    • FlatList 优化参数

练习挑战

第一题 ⭐(基础):画出通信流程

请描述当用户在 RN 应用中点击一个按钮,触发 setState 导致文字变色时,从点击到屏幕变色,整个过程中三个线程分别做了什么?Bridge 被使用了几次?

点击查看答案

完整流程:

  1. UI Thread:捕获触摸事件(touchStarttouchEnd
  2. Bridge 第 1 次:UI Thread 将触摸事件序列化为 JSON,异步发送给 JS Thread
  3. JS Thread:收到事件 → 执行 onPress 回调 → 调用 setState → React reconciliation 算出 Diff → 生成 UI 更新指令(更新文字颜色属性)
  4. Bridge 第 2 次:JS Thread 将更新指令序列化为 JSON,异步发送给 Native 端
  5. Shadow Thread:如果涉及布局变化则重新计算(本例只是颜色变化,不涉及布局,Shadow Thread 可以跳过)
  6. UI Thread:收到更新指令,修改原生 View 的颜色属性,屏幕刷新

Bridge 至少被使用 2 次:一次事件传递(Native → JS),一次渲染指令传递(JS → Native)。

第二题 ⭐⭐(进阶):优化方案分析

你的 RN 项目中有一个可拖拽排序的列表,用户反馈拖拽时非常卡顿。请从 Bridge 架构的角度分析原因,并提出至少两种优化方案。

点击查看答案

原因分析:

拖拽排序涉及两类高频操作:

  1. 手势追踪:手指移动时产生大量 touchMove 事件,每个事件都要 Native → Bridge → JS
  2. 位置更新:JS 计算元素新位置后,要通过 Bridge → Native 更新视图位置
  3. 列表重排:拖拽到新位置时,多个元素需要动画移动,产生大量 Bridge 消息

这三类操作叠加,Bridge 严重拥塞,导致帧率下降。

优化方案:

  1. 使用 react-native-reanimated + react-native-gesture-handler:将手势处理和动画逻辑移到 UI Thread 上直接执行,避免手势事件和位置更新经过 Bridge。这是最有效的方案。

  2. 减少中间状态更新:拖拽过程中只在 Native 端做视觉反馈(通过 AnimateduseNativeDriver),只在拖拽结束后才通过 Bridge 发送最终排序结果给 JS Thread 更新数据。

  3. 使用 Native 实现拖拽组件:对于核心交互体验,直接用 Native Module / Native Component 实现拖拽逻辑,完全绕过 Bridge。

第三题 ⭐⭐⭐(综合):架构设计

假设你需要设计一个新的跨端框架,但不能使用 Bridge(JSON 序列化 + 异步通信)的方式。请提出一种替代方案,解释它如何解决 Bridge 的核心问题。

点击查看答案

这道题实际上在考察你对新架构 JSI 的理解。核心思路是:

方案:基于共享内存的 C++ 中间层(类似 JSI)

  1. 用 C++ 实现一个中间层,同时嵌入到 JS 引擎和 Native 运行时中
  2. JS 引擎通过 C++ binding 直接调用中间层的函数(同步调用,不需要序列化)
  3. 中间层持有 Native 对象的引用,可以直接操作 Native 的数据结构
  4. Native 端也可以通过 C++ 中间层直接调用 JS 函数

解决的核心问题:

  • 去掉序列化:JS 和 Native 通过 C++ 共享内存中的对象引用,不需要 JSON 序列化
  • 支持同步调用:不再是异步消息队列,JS 可以同步调用 Native 方法并立即拿到返回值
  • 按需加载:不需要启动时全量注册,使用时才通过中间层获取模块引用

这就是 RN 新架构中 JSI(JavaScript Interface)的核心设计思路。它用一个 C++ 抽象层取代了 Bridge,从根本上解决了序列化开销和异步延迟问题。


自我检测

读完本章后,对照下面的清单检验一下自己的掌握程度:

  • 能画出 RN 老架构的三层结构图(JS Layer、Bridge、Native Layer),并解释每一层的职责
  • 能说清楚三个线程(JS Thread、UI Thread、Shadow Thread)各自负责什么,以及它们如何通过 Bridge 协作
  • 能解释 Bridge 的通信机制:为什么用 JSON 序列化、为什么是异步批量的
  • 能从序列化开销、异步延迟、Bridge 拥塞三个维度分析 Bridge 的性能瓶颈
  • 能说出 Native Modules 和 Native Components 的区别,并各举一个例子
  • 能从架构角度分析长列表白屏、动画不跟手、JS 繁忙三个典型卡顿场景的原因
  • 能说出至少三种缓解 Bridge 性能瓶颈的实战方案
  • 能解释为什么理解老架构对学习新架构(Fabric + TurboModule)很重要

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90