
1. 从一次白屏排查说起React Native 为什么要引入 Bridge前阵子同事在生产环境遇到一个挺诡异的问题App 启动后首页偶尔白屏三四秒接着才刷出内容。日志打出来一看JS 侧早就执行完了原生侧也在干等中间隔了一层若隐若现的通信延迟。排查到最后问题还是落到了 React Native 的 Bridge 机制上。这已经不是第一次遇到和 Bridge 有关的问题了所以我决定把这块内容彻底梳理一遍写成这个“碎片八股文”系列的第一篇。网上聊 React Native Bridge 的文章不少但多数停留在“JS 和原生通过 Bridge 通信”这句话上。真正让人困惑的是为什么偏偏要搞一个 Bridge为什么不能像普通 API 那样直接互相调用它到底在通信过程中扮演了什么角色又为此付出了什么代价这些问题如果只看官方文档往往得不到特别直白的答案。这篇博文面向的读者是已经在用 React Native 做业务开发、遇到过白屏或卡顿问题、或者准备面试需要把架构讲清楚的人。我会从三个层面展开第一回顾 Bridge 诞生的背景和设计动机第二拆解一条消息从 JS 到原生的完整流程说清楚每一步的取舍第三分析 Bridge 的性能代价以及新架构里 JSI、TurboModule、Fabric 是怎么替代它的。最后附上我自己的白屏排查实录和面试答题思路。很多人以为理解 Bridge 只需要搞清楚“JS 调用原生模块”这一个场景就够了实际上不是。启动阶段的大量初始化、图片缓存、日志上报、事件回调全部在走这条通道。你写业务代码时感觉不到它的存在但它始终在那里角色有点像一个包揽了整栋楼快递业务的物业中心——业主JS和租户原生互不认识所有东西都得经过物业中转。React Native 是 2015 年开源的当时的移动端开发处于原生与 Web 技术激烈碰撞的时期。React Native 想把 React 的声明式 UI 开发体验带到移动端同时保留原生性能。但一个很现实的问题是JavaScript 引擎和原生代码运行在两个完全不同的世界怎么让它们配合干活Bridge 就是当时给出的答案。更准确地说不是“设计者选择了 Bridge”而是在当时的架构条件约束下Bridge 几乎是唯一可行的方案。1.1 启动白屏问题背后的通信链路先说白屏问题。React Native 启动时从用户点击 App 图标到首帧内容呈现中间要经过一条很长的链路原生容器启动、创建 JSCoreJavaScriptCore或 Hermes 引擎、加载 JS Bundle、执行 JS 代码、通过 Bridge 把组件树信息传给原生层、原生层再执行布局计算和渲染。这条链路上任何一个环节卡住都表现为“白屏”。在我遇到的那个案例里问题出在启动阶段有大量模块注册和事件订阅。每个模块的注册要通过 Bridge 走一遍消息通道而且 Bridge 是异步的消息要排队、序列化、跨线程传递。启动瞬间大量消息堆积通信通道挤满了业务 UI 的渲染指令只能排在后面。这就是“JS 执行完了但界面还没出来”的原因之一。如果你只把 Bridge 理解成“一座桥”很容易误解为它的开销只是“走个桥而已”。实际上一次 Bridge 通信的开销包括JS 侧打包参数、JSON 序列化、跨线程发送、原生侧反序列化、方法查找、参数校验、回调注册。每一步都有成本叠加起来就很可观。启动场景下消息数量可能达到几千条这些成本会被成倍放大。1.2 没有 BridgeJS 和原生怎么对话当时的架构约束2015 年前后移动端要想在同一个 App 里同时跑 JavaScript 和原生代码可选的方案非常少。JavaScriptCore 虽然是苹果系统内置的引擎但它只提供一个 C 接口开发者可以把 JS 对象暴露给原生世界也可以通过 JSContext 调用原生函数。问题在于这种做法是同步的、单线程的而且 JS 对象和原生对象之间的引用关系非常脆弱稍有不慎就会造成循环引用或内存泄漏。当时主流方案是 WebView JavaScript Bridge比如 Cordova 的插件机制。它的原理是把原生能力封装成接口通过 URL Scheme 或 JavaScript 注入的方式暴露给 WebView 里的 JS 调用。这个方案成熟度很高但性能上限很低每一次调用都要经过 WebView 的解析和拦截延迟明显无法满足 React Native 对 UI 渲染帧率的要求。React Native 团队最终选择了“在原生侧跑一个独立的 JS 引擎并通过消息机制通信”的架构。这个选择有几个关键考量第一JS 引擎可以独立于 UI 线程JS 执行和原生渲染互不阻塞第二消息通信是异步的天然适配 JS 的单线程事件循环模型第三消息用 JSON 序列化数据结构简单跨语言解析成本可控。 Bridge 的设计正是围绕这三点展开的。2. Bridge 到底是怎么工作的一条消息的完整旅程要理解“为什么用 Bridge”最好的办法是跟一条消息走完全程。假设你写了一句代码调用一个原生模块的方法比如读取设备电量import { NativeModules } from react-native; const { BatteryModule } NativeModules; BatteryModule.getBatteryLevel().then(level { console.log(当前电量, level); });这段代码不是直接调用原生方法的它只是创建了一个调用请求。真正发生的是下面这条链路。2.1 调用一次原生模块消息经历了什么第一步JS 侧的方法调用会被封装成一个消息对象里面包含模块 ID、方法 ID、参数数组以及回调 ID。模块 ID 和方法 ID 是编译期静态分配的编号运行时通过查表定位参数数组则是你传入的实际数据。第二步消息进入 MessageQueue也就是 JS 侧的队列。React Native 不会每产生一条消息就立刻发送而是把多条消息合并成一个批次通过一次性批量传输减少跨线程通信次数。这个设计在启动阶段尤其重要因为启动时往往有几百条初始化消息批量传输能显著降低开销。第三步批次消息经过 JSON 序列化从 JS 线程传递到原生线程。注意这里传递的是一个字符串不是二进制对象。这就是最关键的设计决策之一通过序列化把跨语言的调用变成“数据交换”双方不需要知道对方的内存结构。第四步原生侧收到消息后在 NativeModules 注册表中查找对应的模块和方法反序列化参数调用真实的原生实现。调用完成后如果前面带了回调 ID原生侧会构造一个返回消息通过同样的通道回传给 JS 侧JS 侧再根据回调 ID 触发对应的 Promise 或 callback。这一步一步看起来不复杂但每一步都有隐含的成本。为了更直观地看这成本用一个真实场景来算笔账假设你加载一个列表每个 cell 需要调用一次原生方法获取本地缓存数据。每调用一次大约产生一次 JSON 序列化和反序列化加上两次线程间拷贝。列表有 20 个 cell总共 40 次序列化。如果列表滚动时反复触发这些开销就会变成肉眼可见的掉帧。2.2 队列、批处理和 JSON 序列化Bridge 的三大设计决策及其原因这三个设计不是随意的每一个都对应一个具体的约束。第一队列是异步的因为 JS 是单线程模型。JS 的事件循环机制决定了它不能同步阻塞等待原生返回结果所以 Bridge 必须设计成异步的。这也解释了为什么 React Native 里很多原生调用是异步返回的Promise 就是这种设计的外在表现。第二批处理是为了减少线程切换。JS 线程和原生模块线程之间需要同步每次同步都有锁的开销。如果每一条消息都做一次线程切换高频调用场景下性能会急剧恶化。合并成批次后一次线程切换可以传递几十条甚至上百条消息均摊成本就低得多。第三JSON 序列化是为了让两种语言能“看懂”同一个数据。JavaScript 的对象和 Objective-C / Java 的对象在内存布局上完全不同直接共享内存不现实。JSON 作为中间表示两边都能解析而且格式固定便于调试。缺点也很明显——序列化和反序列化本身要消耗 CPU还会丢失一些类型信息比如 Date、Map 这类特殊对象传递后需要额外处理。从这三个决策能看出一件事Bridge 的本质是个协议而不是一条管道。它定义了双方通信时的语言、规则和格式让两个独立运行的世界可以优雅地协作。代价是每次协作都要“翻译”一遍翻译的效率和准确度直接决定了整个框架的上限。还有一个容易被忽略的细节Bridge 的队列不仅存在于 JS 侧原生侧也有对应的队列。调用请求到达原生侧后会被放到原生消息队列里等待原生模块线程处理。如果原生模块处理速度慢消息堆积会在原生侧发生反过来影响 JS 侧的回调。这也是很多性能问题的根源。3. Bridge 的代价为什么后来大家都在喊“干掉 Bridge”聊完 Bridge 的工作原理很多人会有一个疑问既然 Bridge 能工作为什么新架构要引入 JSI、TurboModule、Fabric 一堆东西去替代它原因很简单Bridge 性能不够好而且它的问题不是修修补补能解决的。3.1 性能瓶颈的根源分析Bridge 最主要的性能瓶颈是序列化和线程切换。先说序列化。JS 侧的对象要变成 JSON 字符串原生侧要把 JSON 字符串解析成原生对象这个过程在每次通信时都会发生。一个简单的字符串hello传过去光序列化的成本可能不大但一张图片的 base64 字符串、一大段日志文本、一个复杂嵌套对象序列化的开销就不可忽视了。再细看一个场景JS 调用原生方法获取通讯录列表返回值是一个包含几千个联系人的数组。序列化这个数组可能要耗费几十毫秒然后在 JS 线程和原生线程之间传递一次再反序列化。如果这个操作在列表滚动时频繁触发性能问题就会非常明显。线程切换的成本同样不可小觑。Bridge 的消息队列需要跨线程同步每次同步要加锁、拷贝数据、唤醒目标线程。这种上下文切换本身不是大问题但频次高起来以后开销就会成倍累积。特别是在帧渲染的关键路径上一次额外的线程切换可能导致掉帧。启动白屏问题也跟这个有关。启动阶段JS Bundle 需要被解析、执行同时要注册大量原生模块每一个模块的注册都需要经过 Bridge 通信。多条消息在队列里排队再加上序列化开销启动时间很容易被拉长几百毫秒甚至更多。3.2 类型不安全与调用约定除了性能Bridge 在开发体验上也有明显短板。因为通信靠的是 JSON 序列化和 ID 查表所以模块 ID、方法 ID、参数类型完全靠约定没有编译期的类型检查。如果你在 JS 侧传错了参数类型通常要等到运行时报错才能发现有时甚至静默失败——原生方法收到一个空值函数继续执行返回一个默认结果你根本不知道数据已经丢了。类型安全问题在团队协作时尤其让人头疼。假设原生侧改了方法签名把参数从字符串类型改成了数字类型JS 侧没同步改通信时序列化没有问题但原生方法内部可能出现意外的行为。这种问题排查起来非常痛苦因为报错信息往往不会指向契约变更的地方。为了解决这个问题React Native 社区引入了 codegen代码生成器的概念通过统一的 schema 定义自动生成 JS 和原生两端的接口代码。但这个方案是后补的Bridge 时代并没有这么好的工具链支持很多团队还是靠手写文档维持双方的接口契约。另外一个容易踩的坑是回调的时机。Bridge 的异步机制意味着原生方法执行完要主动触发回调JS 侧才能拿到结果。如果原生侧忘记触发回调JS 侧的 Promise 就会一直 pending表现出来就是某个功能“卡住了”但实际上原生侧已经执行完了只是结果没有回传。这类 bug 的排查方向很容易走偏因为你在 JS 侧看不到任何异常。3.3 从“为什么用 Bridge”到“为什么要抛弃 Bridge”站在现在的视角看Bridge 是 React Native 早期架构设计的一个合理选择但它不是最优解。随着 React Native 的普及社区对性能的要求越来越高Bridge 的固有缺陷被放大。最核心的问题是通信成本太高无法在关键路径上“频繁调用”原生能力。一个典型例子是手势系统。手势处理需要每一帧都读取原生侧的位置信息如果通过 Bridge 通信每帧都要序列化和线程切换性能根本扛不住。所以 React Native 早期的 Gesture Responder 系统被吐槽很多直到后来引入 JSI原生侧可以直接把数据暴露给 JS 侧读取这个问题才得到缓解。另一个例子是图片加载。图片加载涉及磁盘缓存、网络请求、解码原生侧要做的操作很多。如果每一步都要通过 Bridge 通信光是回调嵌套就能把代码写得非常难看而且性能也不行。新架构下图片的解码结果可以通过 JSI 直接暴露给 JS 侧省去中间多次序列化和线程切换加载速度提升明显。所以Bridge 被替代不是因为它“做错了”而是因为它“做多了”。它的核心问题在于它为每一次通信都套上了一层厚重的协议开销而实际场景中很多通信根本不需要这么重的处理。新架构的思路是把通用协议重的地方去掉改成按需、尽量同步、尽量直接的方式来通信。4. 新架构下 Bridge 的替代者JSI、TurboModule 与 FabricReact Native 新架构从 2018 年开始规划2021 年后逐步在版本中落地。它并没有完全删除 Bridge 的概念但在核心链路上引入了三样新东西JSIJavaScript Interface、TurboModule、Fabric。这三者协同工作解决了 Bridge 最核心的痛点。4.1 JSI共享对象替代消息复制JSI 是整个新架构的基石。它定义了一套 C 接口让 JS 引擎和原生代码可以共享对象而不是像 Bridge 那样靠 JSON 字符串来传递数据。用生活化的类比解释Bridge 时代JS 想把一个数据给原生得把数据写在一张纸上拍照发给原生原生再照着照片把数据重建出来。JSI 时代两边直接坐在同一张桌子前共同看一份原件谁需要谁直接看。JSI 的核心能力是JS 引擎可以通过 JSI 持有原生对象的引用原生代码也可以持有 JS 对象的引用双方可以直接调用对方的方法、读取属性。这样做的好处是序列化和反序列化不再需要了线程切换也大幅减少很多场景下可以做到真正的同步调用。JSI 还支持不同的 JS 引擎后端。以前 React Native 默认绑定 JavaScriptCoreJSI 把引擎解耦之后可以换成 V8、Hermes 等。Hermes 是 Facebook 专门为 React Native 开发的引擎启动速度比 JavaScriptCore 快得多这也是新架构下白屏问题显著改善的原因之一。4.2 TurboModule 与 Codegen按需加载和类型安全TurboModule 是对原生模块系统的重新设计。Bridge 时代所有原生模块在启动时一次性全部注册无论你是不是真的用到它们。这就是启动白屏的一个关键原因模块越多启动越慢。TurboModule 的做法是懒加载。JS 侧第一次调用某个原生模块时才去初始化对应的原生对象。这样启动阶段就完全不需要等待模块注册了只加载必要的模块启动速度自然提升。Codegen 则是从工具链上解决类型安全问题。开发者在 schema 文件里定义原生模块的接口由 codegen 自动生成 JS 侧的 TypeScript 类型声明和原生侧的接口代码。这样两边的调用不再依赖手写契约类型错误在编译期就能暴露出来。这一套组合的实际体感是调用原生模块时的代码提示更智能了参数类型不对会直接报错而且调用效率明显提升。对于做原生模块封装的同学来说写一个 TurboModule 的工作量比写老的 NativeModule 略高但需要手写的重复代码少了很多。4.3 Fabric渲染管线的去 Bridge 化Fabric 是新的渲染系统主要替代老的 UIManager。老的渲染管线上JS 侧把组件树序列化后发给原生侧原生侧再创建对应的原生视图。这个过程要经过 Bridge更新视图时也要走同样的通道性能开销很大。Fabric 把渲染链路改成了基于 JSI 的直接通信。JS 侧可以直接操作原生视图的句柄调用原生视图的更新方法不需要再走消息队列。加上 React 18 的并发特性渲染优先级也可以更精准地控制紧急更新比如输入框的文本可以插队不再被其他批次的消息阻塞。对于开发者来说Fabric 带来的最直接变化是列表滚动更流畅、更新视图的延迟更低、启动时白屏的几率明显下降。但要注意Fabric 是逐步落地的很多老版本 React Native 还是跑在老架构上。如果你遇到性能问题第一步先确认自己用的是不是新架构再排查其他原因。5. Bridge 与启动白屏高频问题的排查实录回到开头那个白屏问题。经过前面的分析你已经知道 Bridge 在启动阶段的角色是关键瓶颈之一。下面是我在实际项目里排查白屏问题的一套方法比较通用值得收藏。5.1 白屏问题排查思路第一步确认 JS Bundle 的加载时间。检查 Metro 打包日志看 Bundle 文件大小和执行时间。如果 Bundle 特别大考虑做分包和按需加载让首屏只加载必要代码。第二步确认原生侧模块注册耗时。在原生侧打点统计所有自动注册模块的初始化时间。如果某个第三方原生模块初始化特别慢考虑把它改成懒加载或者换一个更轻的方案。第三步确认 Bridge 消息队列的堆积情况。打开 React Native 开发者菜单里的性能监控观察 JS 线程和原生线程的占用情况。如果 JS 线程长时间处于忙绿状态说明 Bundle 执行耗时太长如果 JS 线程空闲但页面没渲染大概率是 Bridge 消息队列堆积或者渲染链路阻塞。第四步检查是否有不合理的同步操作。Bridge 时代有个常见错误在原生模块初始化方法里做耗时操作比如同步读取大量用户数据、初始化数据库等。这些操作会把原生线程拖住导致后续所有 Bridge 消息都排队。优化方式很简单把耗时操作放到子线程或者异步队列里。第五步升级到新架构。如果业务场景允许建议直接升级到 React Native 新架构0.72 以上版本开启 Fabric 和 TurboModule很多 Bridge 时代的性能问题会自然消失。5.2 常见问题速查表现象可能原因排查方向解决方案启动白屏 3-5 秒Bundle 过大或执行慢检查 Bundle 体积和 JS 执行耗时分包、按需加载、使用 Hermes启动时偶发白屏原生模块注册阻塞打点统计模块初始化时间懒加载模块、减少自动注册列表滚动卡顿Bridge 高频通信分析调用频次和数据量合并调用、改用 JSI 直通、开启 FabricJS 调用原生模块无响应回调未触发检查原生模块实现确认回调路径完整、打印日志新架构下功能异常模块不兼容检查模块是否支持新架构使用 codegen 重写或替换模块这个表格不是教条是根据实际踩坑总结出来的。实际排查时先按表格里的思路走一遍能解决八成问题。剩下两成就得靠打日志、拆调用链来逐步定位了。6. 面试被问到 Bridge怎么答才能不被问倒这个话题在 React Native 相关的技术面试里出现频率很高。面试官爱问的不仅仅是你知道 Bridge 是什么而是你能不能把原理讲明白、把问题分析清楚。建议的回答结构分四层。第一层概述Bridge 是 JS 和原生通信的桥接层通过异步消息队列传输数据。第二层讲清机制消息经过模块 ID、方法 ID、参数数组封装经过 JSON 序列化、跨线程传递、原生侧反序列化、回调返回强调批处理机制和异步特性。第三层讲问题的本质Bridge 的序列化和线程切换成本高不适合高频通信。第四层讲演进新架构中 JSI 替代了序列化通信TurboModule 解决了模块懒加载Fabric 优化了渲染链路。如果面试官追问“为什么当初不直接用 JSI”你可以回答JSI 依赖 C 层实现早期 React Native 的架构主要基于 Objective-C 和 Java没有统一的 C 层支撑。后来团队重构了底层架构才可能把 JSI 引入。这个回答能体现你对架构演进的理解深度。还有一个容易加分但很多人忽略的细节Bridge 的消息通道是 FIFO 的也就是说消息会按照顺序依次执行。如果有一条消息特别卡顿后续所有消息都会跟着排队。这个特性在新架构中也有但 JSI 的直调可以绕过部分队列约束。能提到这一点说明你真读过源码。面试最忌讳的就是背文档。你可以从自己项目里的一个问题讲起带着问题去讲原理。比如开头那个白屏问题就是很好的引子。面试官想听到的不是完美答案而是你碰到问题时的分析过程以及你能不能在原理层面解释清楚。写在最后这期“碎片八股文”先聊到这里。写这篇文章时我刻意把每个技术的“为什么”都尽量讲透而不是只列结论。因为无论是面试还是实际调优只有理解了背后的设计动机你才能在遇到具体问题时找到正确的排查方向。React Native 的架构还在快速演进Bridge 这个曾经的核心概念在新架构里已经逐渐退居幕后。但理解它依然是理解 React Native 设计哲学的钥匙。下一篇我打算聊聊新架构下 JSI 的更多细节尤其是它在跨端场景中的应用边界。如果你有感兴趣的方向欢迎在评论区聊聊。