React Native的跨端能力来自JavaScript与原生平台之间的消息传递。旧架构下,任何调用都要经过Bridge队列,这条链路在低频操作时几乎无感,但在高频场景里会变成明显的性能瓶颈。理解瓶颈产生的原因,需要从序列化、线程模型和回调机制三个层面入手。

一、Bridge异步通信模型为什么会拖慢性能
旧版React Native使用Bridge作为JS与原生之间的唯一通道。当JavaScript调用NativeModules.SensorModule.startUpdates时,调用并不会直接进入原生方法,而是先被包装成一条消息,放入等待队列。原生线程在合适的时机取出消息,解析参数并执行对应的方法。执行完成后,结果或回调再次经过队列返回JS线程。这个过程本身是异步的,意味着单次调用至少包含一次完整的线程切换和两次参数序列化。
参数序列化是性能开销的重要来源。JS侧的数值、字符串、对象在传给原生前,会被转换成JSON兼容的中间格式;原生侧拿到后,还需要转换成自己的数据结构。对于简单参数,这个开销不大,但如果每帧调用几十次,每次都要创建临时对象、分配内存,GC压力会迅速上升。尤其是在Android平台,频繁的跨线程消息还可能导致UI线程被阻塞,出现明显的丢帧。
NativeModules.SensorModule.startUpdates(100, (data) => {
this.setState({ value: data.value });
});
上面的代码如果放在高频事件循环中,每一次调用都会完整走一遍Bridge。更隐蔽的问题是回调函数也会被序列化后传递,原生侧保存这个回调并在未来某个时刻调用,这进一步增加了内存占用和调用链路的复杂度。要优化,必须先理解这条链路中每一步的成本分布。
二、如何定位原生模块桥接的性能瓶颈
定位桥接性能问题,不能只看整体帧率。帧率下降只能说明存在瓶颈,无法定位到是序列化、线程切换还是原生业务逻辑消耗。可以借助React Native自带的Performance Monitor查看JS线程与UI线程的占用情况,也可以使用Flipper的React DevTools插件观察哪些桥接调用频繁触发。
更精确的做法是在JS侧和原生侧分别埋点,测量同一调用的两端耗时。JS侧可以使用performance.now()计算调用返回所花费的时间;原生侧可以在方法入口和出口记录时间戳。两者相减,如果JS侧总耗时远大于原生侧业务耗时,说明大部分时间被Bridge排队和序列化吃掉;如果原生侧耗时高,则问题在业务实现本身。
const start = performance.now();
await NativeModules.HeavyModule.calculate();
const cost = performance.now() - start;
console.log('bridge cost', cost);
在Android原生模块中,可以用System.currentTimeMillis()记录方法执行时间。iOS则可以使用CFAbsoluteTimeGetCurrent()或os_signpost配合Instruments进行区间分析。通过对比两端数据,能快速判断出桥接开销是否位于关键路径上。建议在真机环境测试,模拟器上的线程调度和序列化表现与真机存在差异。
long start = System.currentTimeMillis();
// 原生业务逻辑
long cost = System.currentTimeMillis() - start;
Log.d("BridgeCost", String.valueOf(cost));
三、降低调用频率:批量处理与事件通道
既然每次桥接都有固定开销,最直接的优化思路就是减少调用次数。很多场景下,连续多次调用是可以合并成一次批量调用的。例如页面初始化时需要读取应用配置、主题色、本地化语言等多个值,如果逐个调用getConfig,会产生多次Bridge往返。改成一次getBatch调用,原生侧一次性返回所有结果,桥接次数从几十次降到一次。
批量调用不仅减少序列化次数,还能让原生侧统一安排数据读取,避免多次线程切换。实现时要注意返回结构的设计,尽量使用扁平的Map或Array,减少嵌套层级,否则原生侧生成返回对象时又会引入新的序列化开销。
const result = await NativeModules.ConfigModule.getBatch(['theme', 'locale', 'env']);
this.setState({
theme: result.theme,
locale: result.locale,
env: result.env,
});
对于需要持续推送数据的高频场景,例如传感器、进度条更新,使用回调函数并不理想。可以改用NativeEventEmitter事件通道,原生侧通过sendEvent向JS广播数据。事件通道虽然也走Bridge,但它可以配合节流策略,在原生侧控制发送频率。比如传感器每10毫秒产生一次数据,但UI只需要每100毫秒刷新一次,就可以在原生侧做抽样,再统一发出事件,避免JS侧被高频消息淹没。
import { NativeEventEmitter, NativeModules } from 'react-native';
const emitter = new NativeEventEmitter(NativeModules.SensorModule);
const subscription = emitter.addListener('onSensorChange', (data) => {
// 这里做UI更新
});
Android侧发送事件的代码可以用sendEvent方法,注意事件名称要与JS侧一致。还可以在原生侧加入时间窗口,累计多个数据点后一次性发送数组,进一步提升传输效率。优化目标不是完全消除桥接,而是把桥接频率降到帧周期可以接受的范围。
四、迁移JSI与TurboModules获得同步调用能力
旧Bridge最大的限制在于异步和序列化,而新架构中的JSI(JavaScript Interface)改变了这一模型。JSI允许JavaScript直接持有C++对象的引用,调用方法时不再经过消息队列和JSON序列化,而是通过虚函数表直接分发到原生实现。这意味着某些原本必须异步等待的原生调用,可以变成同步调用,并且参数传递的成本大幅降低。
TurboModules是基于JSI的模块系统,它保留了NativeModules的调用方式,但在底层用JSI绑定替换了Bridge。对于性能敏感的原生模块,迁移到TurboModules是收益最高的优化手段。迁移时不必一次性重写整个应用,可以优先处理那些被高频调用、参数复杂或需要同步返回结果的模块。例如一个负责数学计算的原生模块,在旧架构下每次调用都要序列化输入和输出;迁移到TurboModules后,参数以JSI对象形式在C++层传递,无需JSON字符串转换。
jsi::Value getConstant(jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) {
return jsi::String::createFromUtf8(runtime, "hello");
}
JSI不是银弹,它要求团队具备一定的C++基础,并且新架构的调试工具链还在完善。但对于已经被Bridge性能问题严重困扰的项目,分模块迁移是一条现实路径。即使不迁移,也可以参考新架构中同步调用的思路,将一些不依赖UI线程的操作放到原生后台线程执行,再用事件回传,这同样能减轻Bridge压力。
最终,性能优化是持续权衡的过程。理解Bridge的通信成本、建立测量习惯、优先优化高频调用,再结合新架构逐步替换,才能让React Native在复杂场景下保持流畅。

