RN 的新架构综述

0 阅读10分钟

前言

这是本专栏关于 RN 框架的第 8 篇文章了,前 7 篇文章中,我们详细的聊了 RN 旧架构(Bridge)是如何实现的,以及进行了哪些权衡

在接下来的文章中,我们将开始学习 RN 的新架构(JSI),我希望用这篇文章来回答三个问题:

  1. RN 为什么需要一个新的架构?
  2. RN 是如何平滑切换新旧架构的?
  3. 新架构解决了哪些问题?

时间线

New Architecture is here 这篇官方博客中,简单介绍了 RN 新架构的一些变化与新功能

本小节我们通过时间线来了解一下 RN 团队是如何完成这次巨大的架构更新的

timeline

我们先简单过一下时间线:

  1. 在 2015 年的时候 RN 首次开源,推出了基于 Bridge 的初代架构
  2. 在 2018 年的时候,RN 团队决定要重新设计整个 RN
  3. 在 2018 年中,他们在其中三个版本引入了新架构的主要更新:JSI、Fabric、TurboModule
  4. 在 2021 年,引入了最后一个主要模块 RuntimeScheduler
  5. 经过了 4 年的迭代完善,新架构在 2022 年的 0.68 版本被正式加入选择性开关 opt-in switch,这意味着开发者可以低成本的接入新架构了
  6. 经过 2 年的验证,新架构在 2024 年的 0.76 版本被当成默认模式使用
  7. 最后在 2025 年,旧架构代码在 0.68 被全部移除,只保留新架构代码,架构升级完成

整个框架重构,RN 团队用了 8 年的时间。这次重构绝对不是单纯的技术替换,而是一次横跨运行时、渲染引擎、以及框架生态的系统级重构

其中的关键模块:Fabric、JSI、TurboModule、RuntimeScheduler 并非一次性发布,而是经过了渐进式引入、双架构并行、opt-in 验证、默认启用、最终移除这样的阶段性策略来完成的

这也给我们进行框架重构带来了一些启示:

  • 不要试图 “一次性推翻重来”
  • 先抽象核心边界(通信层的 JSI、调度层的 RuntimeScheduler、渲染层的 Fabric)
  • 允许新旧系统长期并存
  • 提供明确的迁移路径
  • 让生态先跑起来,再宣布成功

这不仅仅是技术能力,还是对工程节奏的管理能力


这种重大的决策动机往往来源于外部,我个人猜测大概率与在 2017 年 React 16 引入的 Fiber 架构有关。Fiber 本质上是把调度权收回了 React 框架内部,使得后续的 Concurrent rendering 成为可能,而基于 Bridge 的旧架构天然无法支持这种调度模型,这在理念上推动了 RN 的框架底层重构

旧架构的问题

本章节主要内容主要来源于 New Architecture is here,并加入了我个人的理解

我们都知道,RN 旧架构采用了异步的 Bridge + Batched Queue 作为 JS 与 Native 世界的通信方式,这个方案的好处就是 App 的主线程永远不会因为渲染更新或者 Native modules 调用而阻塞

相对应的,也带来了以下问题:

  1. 在一些需要及时响应的场景,无法同步调用甚至中断进行中的渲染
  2. 所有的跨层通信都需要序列化,在高频更新或传递大量数据时带来显著性能开销与延迟,使得很难保证可靠的 60+ FPS
  3. JS 与 Native 无法同步协调导致中间状态被错误渲染出来造成页面闪动
  4. 在 Native 侧只有一份可变(Mutable)的 Native UI 树,并且每次更新它都只能进行原地修改,这导致了 layout 只能在单线程种串行计算,既无法处理紧急更新(比如用户输入)、也无法在 JS 种同步读取最新的布局信息

总结一下,旧架构问题的根源在于:JS 与 Native 之间是异步消息桥接,使得渲染是单线程、单版本、且不可中断的

这带来了:

  1. JS 侧无法同步读取布局
  2. 无法支持 React 16 提出来的 Fiber 架构以及后续的 Concurrent rendering
  3. 无法支持优先级调度
  4. UI 可能出现中间状态

新架构的四个核心重构

RN 的新架构并非是一次简单的性能优化,而是一次系统重构

新架构从通信方式入手,逐步重写原生能力访问方式渲染模型以及任务调度机制,最终使得 RN 得以具备支持 React 18 并发特性的能力

接下来我们分别介绍一下这四个关键部分

通信模型优化:移除 Bridge,引入 JSI

JS 与 Native 之间异步通信是旧架构的核心痛点,不仅带来了性能的开销、也限制了跨层的同步访问能力

新架构通过引入 JSI(JavaScript interface)彻底替代了 Bridge

RN JSI vs Bridge

此举带来了如下好处:

  1. JSI 允许 JS 与 C++ 层互相直接调用,提升了通信性能
  2. JSI 支持同步 + 异步混合调用
  3. JSI 直接函数调用代表更明确的调用路径与更好的错误堆栈,减少 undefined behavior 和崩溃风险
  4. 移除 Bridge 减少了初始化阶段(Bridge 本身以及 Native modules)的开销,应用启动性能提升
  5. 为新模块系统 Turbo module 提供基础
  6. 为新渲染器 Fabric 提供结构支持

原生能力访问优化:新的原生模块系统 Turbo module

在旧的架构中,原生模块系统有两个问题:

  1. 所有的原生模块都依赖 Bridge,如果我们想要处理来自原生函数调用的响应,需要提供一个回调函数并保证返回值是可以被序列化的
  2. RN 只能在启动时一次性加载所有模块,增加了应用的冷启动性能开销

新的架构使用 C++ 重写了整个原生模块系统,基于 JSI 提供了新的能力:

  • 支持 JS 与原生运行时的同步访问
  • 引入 Codegen 自动生成类型绑定代码,解决 Bridge 序列化导致的类型不安全问题
  • 可以直接用 C++ 编写原生模块,享受 C++ 的跨平台优势
  • 模块默认按需加载,减少应用冷启动时间

渲染模型优化:新的渲染器 Fabric

有读过 RN 代码的读者可能会知道,RN 内部用 Paper 这个代号指代旧的渲染器

从架构上来说,Paper 指的不是某个类或者某个原生模块,而是整条渲染链路:

BridgeUIManager (Two separate implementations for iOS and Android respectively)
  ↓
Shadow Tree (mutable)
  ↓
UI blocks
  ↓
Native View Tree

同样的,新的渲染器 Fabric 重构的也是整条链路:

JSI
  ↓
C++ Shadow Tree (immutable)
  ↓
Commit (version switch)
  ↓
Mounting layer
  ↓
Native View Tree

核心改动点如下:

  1. 渲染核心被下沉到统一的 C++ 层:Shadow Tree 不再是平台相关的对象模型
  2. 将新 Shadow tree 的结构设置为不可变 immutable允许多版本共存

前者使 RN 不再依赖平台特定的对象模型与线程实现,而能够在跨平台共享的运行时中实现一致的状态管理与并发调度能力;后者保证了新 Shadow tree 的线程安全特性,使得 RN 支持同步读取、多版本并行、优先级调度这些对 React Concurrent 渲染至关重要的能力

有了这些能力后,RN 的渲染模型才能完成从 “命令驱动“”状态驱动“ 的重构

调度模型优化:支持新的事件调度模型 RuntimeScheduler

在旧的架构中,JS 线程更像是一个 “消息驱动的 JS 执行环境“,一个典型的渲染流程是:

JS 接收 Native 的消息
  ↓
JS 执行更新逻辑
  ↓
JS 通过 Bridge 将更新指令发回 NativeNative 触发渲染流程

这带来两个问题:

  1. 一些需要同步获取/感知布局信息的能力(比如 ref、useLayoutEffect 等需要在 commit 后同步读取布局的能力)的执行时机是无法被保证的
  2. 没有对齐 Web 中 task / microtask 执行语义(微任务需保证在下一个 render 前执行),导致 RN 与浏览器行为存在差异

这两个问题本质上是 JS 无法感知 Native 的渲染状态导致的,因为 JS 把重要的 ”调度权“ 交给了宿主环境

而解决方案也很简单,就是模仿浏览器,在宿主层引入一个符合 Web 规范的事件调度模型(Event loop model)把调度权拿回来

这么做有几个好处:

  1. 能让 RN 框架的行为更加可预测,帮助开发者构建可靠性更好、性能更佳的应用
  2. 加强框架对 web 规范以及 React 编程模型的对齐,保证框架在跨平台上的一致性
  3. 提供渲染中断、优先级调度的能力

新架构的迁移

上面我们说了这次新架构的核心变化,接下来我们聊聊作为一个应用开发者,如果我们决定要升级到新版本,需要做哪些适配

对于应用开发者来说,迁移工作的核心只包含三件事:

  1. 升级 RN 版本
  2. 确保三方库兼容性(尤其是涉及原生模块的部分)
  3. 迁移自定义的原生模块

事实上,对于大多数的应用开发者来说,升级到新架构几乎不需要修改 JS 代码,因为新架构改变的是 JS 与 Native 的通信方式,而非 JS 的编程模型

但是,对于原生模块开发者,或者是自己的 APP 中使用了自定义原生模块的开发者,就需要迁移(重写)原生模块

当然为了让旧模块低成本接入新架构,RN 团队提供了一个 Interop layer 来兼容旧的 Native Modules 与 UI Components,使它们仍然可以在新架构下运行,但这不意味着用了它就完成迁移了,它只是一个在应用保持正常运行的前提下能够逐步完成迁移的设计。对于 RN 生态中大量的三方库来说,这个兼容层保证了他们有充足的时间来完成新架构适配

从工程角度来说,这个兼容层提供了应用开发者迁移的平顺性,减少了迁移过程中的心智负担

这也是我认为优秀的框架开发者在升级过程中需要做到的:不要求开发者迁移代码,而是让他们尽可能感觉不到升级

总结一下

这篇文章中我们介绍了 RN 旧架构的问题,新架构的改动范围以及 RN 团队是如何管理这次模型迁移的

由于是综述,我们只在很浅的层面上聊了新框架做了什么与为什么这么做,至于具体的实现,乃至核心代码梳理受限于篇幅就留到后面的文章聊啦,希望各位喜欢