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 的核心职责:
- 执行 JS Bundle(你的业务代码)
- 运行 React 的 reconciliation(Diff 算法)
- 计算出 UI 变更指令,通过 Bridge 发送到 Native 端
- 处理来自 Native 端的事件(触摸、滚动等)
关键点:JS Thread 是单线程的。 这意味着如果你的业务逻辑计算量很大(比如大量数据处理),会直接阻塞 UI 更新指令的发送,导致界面卡顿。
2.2 UI Thread(Main Thread)
UI Thread 就是操作系统的主线程,也叫 Main Thread。在 iOS 上是 UIKit 的执行线程,在 Android 上是 Activity 的 UI 线程。
它的职责:
- 执行真正的原生 UI 渲染(创建、更新、销毁原生视图)
- 处理用户手势和触摸事件
- 执行原生动画
所有涉及 UI 的操作必须在这个线程上执行,这是 iOS 和 Android 的系统级要求。
2.3 Shadow Thread
Shadow Thread 是很多人不太了解但非常重要的一个线程。它运行的是 Yoga 布局引擎(Meta 开源的跨平台 Flexbox 布局库,用 C++ 实现)。
它的职责:
- 接收 JS Thread 发来的布局描述(Flexbox 属性)
- 计算每个节点的实际位置和尺寸(left, top, width, height)
- 将计算结果发送给 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 后,调用 UIManager 的 createView 方法,传入解析后的参数。
反方向也一样。当 Native 端产生了一个触摸事件,它会序列化成 JSON 发送给 JS 端。
3.2 异步批量通信
Bridge 的通信是异步的,而且为了减少通信次数,采用了批量处理策略。
具体来说:
- JS 端的渲染指令不会立即发送,而是先积累在一个队列中
- 每隔一个 tick(通常是每帧,约 16ms),将队列中的所有指令打包成一个 JSON 数组一起发送
- 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。
工作方式:
- Native 端编写一个模块类(iOS 用 Objective-C/Swift,Android 用 Java/Kotlin)
- 用 RN 提供的宏/注解将方法标记为可导出
- 应用启动时,所有标记的模块和方法在 Bridge 上注册
- 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)}
/>
工作方式:
- Native 端创建一个 ViewManager,定义支持的属性和事件
- JS 端通过
requireNativeComponent包装成 React 组件 - React 渲染时,通过 Bridge 发送创建/更新指令给 Native 端
- 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 的完整链路。异步通信让每一帧的更新都有延迟。
缓解方案:
- 使用
AnimatedAPI 的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)
- 分批处理数据,使用
requestAnimationFrame或setTimeout让出 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 只适用于一部分动画属性(opacity、transform 等),对于布局相关的属性(width、height、marginTop 等)是不支持的。因为布局变化需要重新计算所有受影响元素的位置,这必须经过 Yoga 布局引擎,无法完全在 UI Thread 上完成。想要更灵活的原生动画,需要使用 react-native-reanimated 这样的库。
误区四:“RN 老架构已经过时了,不用了解”
虽然新架构(Fabric + TurboModule)已经推出,但截至目前,大量线上项目仍在使用老架构。而且理解老架构的问题,才能真正理解新架构每一项改进的动机。面试中,“老架构有什么问题 → 新架构怎么解决的”是一个非常经典的考察链路。
小结
本章从 RN 老架构的三线程模型出发,完整拆解了 Bridge 的通信机制和性能瓶颈。
核心要点
- 三线程模型:JS Thread(业务逻辑)、UI Thread(原生渲染)、Shadow Thread(Yoga 布局计算),三者通过 Bridge 协作
- Bridge 的通信方式:JSON 序列化 + 异步批量传输,双向通信
- 性能瓶颈的三个层面:序列化开销、异步延迟、Bridge 拥塞
- Native Modules:暴露原生功能给 JS,启动时全量注册,异步调用
- Native Components:封装原生 UI 组件为 React 组件,属性更新通过 Bridge
- 常见卡顿场景:长列表白屏(异步延迟)、动画不跟手(通信延迟)、JS 繁忙(单线程阻塞)
记忆口诀
RN 老架构三句话:三个线程各管各,一座 Bridge 全靠它,JSON 异步是代价。
本章思维导图
- 三线程模型
- 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 被使用了几次?
点击查看答案
完整流程:
- UI Thread:捕获触摸事件(
touchStart→touchEnd) - Bridge 第 1 次:UI Thread 将触摸事件序列化为 JSON,异步发送给 JS Thread
- JS Thread:收到事件 → 执行
onPress回调 → 调用setState→ React reconciliation 算出 Diff → 生成 UI 更新指令(更新文字颜色属性) - Bridge 第 2 次:JS Thread 将更新指令序列化为 JSON,异步发送给 Native 端
- Shadow Thread:如果涉及布局变化则重新计算(本例只是颜色变化,不涉及布局,Shadow Thread 可以跳过)
- UI Thread:收到更新指令,修改原生 View 的颜色属性,屏幕刷新
Bridge 至少被使用 2 次:一次事件传递(Native → JS),一次渲染指令传递(JS → Native)。
第二题 ⭐⭐(进阶):优化方案分析
你的 RN 项目中有一个可拖拽排序的列表,用户反馈拖拽时非常卡顿。请从 Bridge 架构的角度分析原因,并提出至少两种优化方案。
点击查看答案
原因分析:
拖拽排序涉及两类高频操作:
- 手势追踪:手指移动时产生大量
touchMove事件,每个事件都要 Native → Bridge → JS - 位置更新:JS 计算元素新位置后,要通过 Bridge → Native 更新视图位置
- 列表重排:拖拽到新位置时,多个元素需要动画移动,产生大量 Bridge 消息
这三类操作叠加,Bridge 严重拥塞,导致帧率下降。
优化方案:
-
使用
react-native-reanimated+react-native-gesture-handler:将手势处理和动画逻辑移到 UI Thread 上直接执行,避免手势事件和位置更新经过 Bridge。这是最有效的方案。 -
减少中间状态更新:拖拽过程中只在 Native 端做视觉反馈(通过
Animated的useNativeDriver),只在拖拽结束后才通过 Bridge 发送最终排序结果给 JS Thread 更新数据。 -
使用 Native 实现拖拽组件:对于核心交互体验,直接用 Native Module / Native Component 实现拖拽逻辑,完全绕过 Bridge。
第三题 ⭐⭐⭐(综合):架构设计
假设你需要设计一个新的跨端框架,但不能使用 Bridge(JSON 序列化 + 异步通信)的方式。请提出一种替代方案,解释它如何解决 Bridge 的核心问题。
点击查看答案
这道题实际上在考察你对新架构 JSI 的理解。核心思路是:
方案:基于共享内存的 C++ 中间层(类似 JSI)
- 用 C++ 实现一个中间层,同时嵌入到 JS 引擎和 Native 运行时中
- JS 引擎通过 C++ binding 直接调用中间层的函数(同步调用,不需要序列化)
- 中间层持有 Native 对象的引用,可以直接操作 Native 的数据结构
- 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