核心概念与架构
上一章把项目跑起来了,这一章回答"跑起来的到底是什么"。React Native 的架构在过去几年经历了整个移动端框架里最剧烈的一轮重构——Bridge 移除、Fabric/TurboModule 默认、Hermes 引擎换代——理解这条时间线,比记住任何一个 API 都更能帮你用好 RN,也更能在升级、排查崩溃、评估第三方库时做出正确判断。读完这一章你应该能回答三个问题:JS 代码怎么变成原生视图;一条 JS 调用要穿过哪几层;以及为什么 0.82 之后"旧架构兼容"这个问题彻底消失了。
React Native 是什么:先破三个误解
RN 的口号是 “Learn Once, Write Anywhere”——不是 Write Once, Run Anywhere。它复用的是你的 React 心智模型(组件、state、effect、数据流),而不是复用一个跨平台渲染器。
误解一:RN 是 WebView 套壳。 不是。Cordova/Capacitor 那条路线是把整套 HTML/CSS/DOM 塞进系统浏览器控件里跑,RN 里没有 WebView、没有 DOM——<View> 在 iOS 上编译产物运行时对应 UIView,在 Android 上对应 android.view.ViewGroup,<Text> 对应 NSTextField/TextView,滚动列表用的是系统原生滚动容器。手势、无障碍、键盘行为、滚动回弹这些都是系统自己实现的,这也是 RN 应用"看起来不像网页"的根本原因。(RN 里确实也有 WebView 组件,那是给加载第三方 H5 用的普通组件,不是渲染底座。)
误解二:RN 是"编译成 Java/Swift"。 不是。RN 没有把 JS 转译成平台语言,也不生成原生代码参与编译。应用里打包的是 JS bundle + 一个 JS 引擎(Hermes)+ 一组预编译好的原生渲染框架,运行时由 JS 描述"视图树长什么样",原生层负责把它变成真实视图。所以 RN 才有"改 JS 可以热更新、加原生模块必须重新出包"这条分界线(热更新本身见 11-构建与发布.md)。
误解三:RN 和 Flutter 是一回事。 都是"写一遍出两端",但分层哲学相反。Flutter 自带渲染引擎(Impeller/Skia),它在系统提供的画布上用 Dart 代码"画"出每一个像素的按钮,不用系统控件——好处是两端视觉绝对一致、性能模型统一,代价是与系统集成(键盘、辅助技术、平台新控件)要自己追赶。RN 反过来:视图就是系统视图,平台感与系统能力"白嫖",代价是布局与外观的细微差异仍要按平台处理。对一个要接大量原生 SDK(支付、地图、推送)的业务来说,RN 这种"以原生视图为一等公民"的模型摩擦更小;对一个追求定制绘制的产品,Flutter 的画布模型更顺手。没有优劣,只有匹配。
一句话定位:RN = React 的编程模型 + 原生视图的渲染后端 + JSI 之上的一层薄运行时。 下面三层逐个拆开。
一段 JSX 是怎么变成原生视图的
写代码时你感受不到任何"跨语言",先看一个再普通不过的组件:
import { Text, View } from 'react-native';
type Props = { name: string; online: boolean };
export function Welcome({ name, online }: Props) {
return (
<View style={{ flexDirection: 'row', alignItems: 'center', padding: 16 }}>
<View
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: online ? '#22c55e' : '#94a3b8',
marginRight: 8,
}}
/>
<Text style={{ fontSize: 16, color: '#0f172a' }}>你好,{name}</Text>
</View>
);
}它变成屏幕像素的路径是:
- JSX 经 Babel 转换后就是
React.createElement('View', …)形式的调用——和 Web React 完全一致,没有任何 RN 专属语法; - React 协调器(reconciler,与 react-dom 用的是同一个 React 内核)算出"哪些节点增删改了";
- 差异交给 RN 自己的渲染器实现(0.82 起即 Fabric),按映射表实例化真实视图:
View → UIView / ViewGroup、Text → 文本控件; - Yoga 把那坨 inline style 解释成布局约束算出坐标,原生层设置 frame/LayoutParams 上屏。
对照着记:Web 世界是 React → react-dom → DOM 节点 → 浏览器绘制;RN 世界是 React → Fabric → 原生视图 → 系统绘制。 换掉的只是最底下"宿主"一环,你写的组件、hooks、状态管理一行都不用变——这正是 “Learn Once, Write Anywhere” 的技术注脚。样式那一步的约束(默认 flexDirection: 'column'、没有 CSS 级联)容易踩坑,单独见 03-样式与布局.md。
线程模型:为什么 JS 卡住时 ScrollView 还能滚
先把一个典型 RN 应用的线程地图画出来(新架构,0.88):
| 线程 | 跑什么 | 卡住的后果 |
|---|---|---|
| JS 线程 | Hermes 执行你的 React 代码:state 更新、effect、事件回调、布局请求的发出 | 交互无响应、数据不更新,但已渲染的原生视图仍可自带滚动 |
| UI 线程(主线程) | iOS/Android 系统主线程:真实视图树、手势识别、Fabric 提交渲染结果 | 整个 App 冻结、丢帧、系统弹 ANR |
| 原生/后台线程 | 布局计算(Yoga)、原生模块工作(网络、文件、数据库)、图片解码 | 通常只拖慢对应功能,不冻结界面 |
画成一张简图(0.88 新架构下的典型形态):
┌─────────────── 设备 ───────────────┐
│ │
│ JS 线程 UI 线程(主线程) │
│ ┌───────────┐ ┌────────────┐ │
│ │ Hermes │ JSI │ 系统渲染 │ │
│ │ React 组件│◄────►│ 真实视图树 │ │
│ │ state/ │ 直调 │ 手势/滚动 │ │
│ │ effect │ │ Fabric 上屏 │ │
│ └─────┬─────┘ └─────▲──────┘ │
│ │ 调用 │ 挂载 │
│ ┌─────▼──────────────────┴─────┐ │
│ │ 原生/后台线程 │ │
│ │ TurboModule 队列 · Yoga 布局 │ │
│ │ 网络/文件/图片解码 │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘前端工程师习惯"一个主线程包打天下"的模型,RN 刻意把它拆开了,这是它性能心智的核心。回扣到标题那个现象:
当你在 JS 线程上跑一个同步死循环,页面里的 ScrollView 依然能滑、动画(走原生驱动的)依然能播、原生的导航返回手势依然可用。
原因:滚动是 UI 线程上系统控件自带的行为。Fabric 把视图树提交给系统之后,滚动、回弹、手势识别由系统自己处理,完全不回调 JS。JS 卡住影响的是"需要 JS 决策的部分"——滚动到底加载下一页、点按后的状态变化、以及 JS 驱动的动画。这也是为什么列表加载下一页在 JS 繁忙时会明显迟钝:视图还能滑,但"滑到头该取新数据"这件事要问 JS。
由此推出几条贯穿全书的工程原则:
- JS 线程是你的主战场,别把重活扔给它。 大数组的排序、JSON 深解析、密码学计算这类明显同步重活,要么拆小、要么交给原生侧(见
09-原生模块与平台代码.md),要么挪进后台任务(见08-存储与文件.md)。 - 动画和手势尽量走原生侧。 0.85 引入的全新 Animation Backend(C++ 事件驱动)让声明式动画基本不经 JS 线程(见
07-动画与手势.md);第三方手势库同理。 - 原生模块不阻塞 UI 线程不等于随便调。 TurboModule 方法可能在调用线程同步执行,写重型同步原生方法会卡 JS 线程——原生模块该怎么设计,
09-原生模块与平台代码.md详谈。
架构演进时间线:一条主线看懂 RN 的来路
RN 的历史就是"把那层 JS 和原生之间的胶水一路换掉"的历史。这条线是本手册的骨架,每一站按"当时卡在哪 → 换上了什么 → 解决了什么"讲。
第一代:Bridge(~0.62 及以前)
最初 RN 的全部跨语言通信只有一条管道:JS 侧 NativeModules.Xxx.method(args) 被序列化成 JSON 字符串,经消息队列批量丢给原生侧,原生解析执行、结果再序列化丢回来。三个结构性毛病:
- 异步且昂贵——所有调用天生异步;每条消息都过 JSON 序列化,批量大时(一帧里几十个布局更新)序列化本身吃掉主线程。
- 全量注册——启动时所有 NativeModule 构造完毕并占用 JS 上下文,哪怕一个都没用到,直接拖累冷启动。
- 没有同步原语——JS 无法持有"原生对象引用",
measure这类需要立刻拿结果的场景只能用异步回调绕。
Bridge 架构下三线程的边界是硬约束:JS 与原生永远隔着一条异步管道。这个天花板最终由下一站的 JSI 凿穿。
第二代:JSI(0.64 起普及)
JSI(JavaScript Interface)不是"更快的 Bridge",是换了问题域:它是 C++ 层的一个引擎无关接口,允许原生侧向 JS 上下文里直接注入宿主对象——JS 拿到的不再是一个"发消息的把手",而是指向 C++ 对象的引用,可以同步调用方法、同步读写属性,不需要 JSON 序列化。注意两点:JSI 与引擎解耦(Hermes/JSC 都能实现);JSI 本身不是模块系统,它只是让"原生对象出现在 JS 里"成为可能。0.64 把 Hermes 和 JSI 的接入打磨成稳定可用,新架构各组件此后陆续建在 JSI 上。
第三代:TurboModule、Fabric、Bridgeless
基于 JSI,三件套分别替换 Bridge 的三个职责面:
- TurboModule 替换 NativeModule 注册与调用机制。模块用 TypeScript spec 声明接口,Codegen 编译期生成原生绑定;模块按需懒初始化——用到的那一刻才创建,启动成本从"全量注册"变成"零"。调用仍是 JSI 同步/异步直调,没有序列化。
- Fabric 替换渲染管道。React 的提交产物先进一棵Shadow Tree(影子树):C++ 里的虚拟视图树,挂着 Yoga 布局计算(在后台线程做),算完的布局结果再同步到真实视图树、由 UI 线程上屏。这层结构让"布局不占 UI 线程"和"渲染更新可批处理、可中断"成为可能,也是 Concurrent React(
startTransition等)能落到原生渲染上的前提。 - Bridgeless 替换启动装配。React Native 的 JS 运行时装配不再依赖 Bridge 对象,直接走 JSI 宿主对象——Bridge 作为"唯一入口"的历史使命结束。
落地节奏:0.76 默认 → 0.80 冻结 → 0.82 移除
时间线上四个关键节点(官方口径):
- 0.76:新架构默认开启。 同时提供 interop 兼容层,让只适配旧架构的第三方库过渡可用。这个版本是生态的分水岭:库作者必须在新架构下测试。
- 0.80:Legacy Architecture freeze(旧架构冻结)。 官方声明旧架构不再演进,只维持现状。
- 0.82:彻底移除旧架构,官方博客标题 “A New Era”——首个完全运行在新架构上的版本,整个框架围绕 JSI 重构。从此"要不要开新架构"“这个库支持新架构吗"不再是立项风险,升级决策从架构问题回归到依赖版本问题。
- 0.88 的收尾细节:仍保留
RCT_REMOVE_LEGACY_MODULE_INTEROP/RCT_REMOVE_LEGACY_COMPONENT_INTEROP两个编译开关(默认关闭),官方说明未来版本会把"移除桥兼容层"变成默认——给还赖在 interop 上的极端存量库的最后窗口。
第四代:运行时与引擎(0.84 起)
- 0.84:Hermes V1 成为默认引擎(详见下一节),冷启动与基座性能再切一刀。
- 0.85:全新 Animation Backend——C++ 事件驱动的动画执行层;
@react-native/jest-preset同期拆为独立包。 - 0.86:Edge-to-Edge 与 DevTools 改进——适配 Android 15+ 强制 edge-to-edge,官方博客标题"no breaking changes”。
- 0.87:Strict TypeScript API 成为默认——对
react-native/Libraries/*的深导入报类型错误;Metro 更新;实验性 Swift Package Manager 支持;AGP 9 支持。 - 0.88(2026-10):Web-Based Event Dispatching——触摸事件派发路径重构(向 Web 的事件模型心智靠拢),外加 Variable Fonts、iOS SceneDelegate 生命周期支持、
InputAccessoryView弃用等;博客标题同样是"no breaking changes"。
一句话总结这条主线:Bridge 解决"能不能通信",JSI 解决"通信值不值得",TurboModule/Fabric 解决"通信与渲染的架构债",0.760.82 解决"生态如何统一",0.840.88 解决"默认性能好不好"。 今天(0.88)写新项目不再需要为旧架构做任何心理建设,读老教程(0.75 年前)时把里面的架构担忧全部作废即可。
通信机制拆解:一次调用的完整旅程
时间线讲完了,把新架构下两条最常走的调用路径拆开看,以后排查"原生模块没回调"“渲染延迟"类问题时脑子里要有这张图。
TurboModule vs 旧 NativeModule
旧 NativeModule:启动时全量注册到 Bridge,调用 = 序列化 JSON + 入队 + 异步送达。TurboModule:
- 你在 TS 里声明 spec(继承
TurboModule,标注方法签名); - 构建期 Codegen 读出 spec,生成 iOS/Android 的 C++ 绑定和类型桩——这就是"接口先行”:JS 和原生的契约在编译期核对,拼错参数名从"运行时 undefined"变成"构建失败";
- 运行期第一次访问该模块时,JSI 宿主对象懒创建并注入 JS 上下文,之后的调用是同步的直调(方法体是否异步由实现决定)。
先睹为快——spec 在 JS 侧长这样(完整可运行版本,含原生实现与注册,在 09-原生模块与平台代码.md):
// spec/NativeDeviceInfo.ts —— JS 与原生之间的编译期契约
import { TurboModuleRegistry } from 'react-native';
import type { TurboModule } from 'react-native';
export interface Spec extends TurboModule {
getBatteryLevel(): Promise<number>;
isDeviceLocked(): boolean; // 同步方法也合法,走 JSI 直调
}
// 取回宿主对象;声明给 Codegen 用
export default TurboModuleRegistry.getEnforcing<Spec>('NativeDeviceInfo');这份 spec 同时喂养三头:Metro/构建期的 Codegen 读它生成 Swift/Kotlin 绑定;iOS/Android 的实现方按它写类;TS 编译器按它检查你的调用。这正是"接口先行"的实际形态——拼错方法名、传错参数类型在构建期就失败,而不是等运行时 undefined 或者拿到一坨序列化过的 NSDictionary 才发现类型不对。
这里只需记住一条:第三方库如果核心还是旧式全量注册 + JSON 桥,说明它没适配新架构,0.82+ 上不可信。
Fabric 与 Shadow Tree
React 一次 commit 在 Fabric 下的旅程:
- React 协调出 UI 变更,交给 Fabric 的 C++ 层,更新 Shadow Tree(每个节点是一个"影子视图",带 props 和样式,但不是真实系统视图);
- Shadow Tree 上做 Yoga 布局计算——flex 样式被解释成真实约束、算出每个节点的 x/y/w/h,这一步跑在后台,不占 UI 线程;
- 布局结果挂载(mount):差异化的节点更新派发到平台层,创建/调整真实
UIView/View; - UI 线程执行系统渲染。
和 Bridge 时代的关键差别:布局与渲染状态由 C++ 统一持有,JS↔原生不再传"整棵树的序列化快照",而是同步结构化数据;React 19 的并发特性(优先级、可中断)有了原生侧的配合点。你平时感受不到它,感受得到的是:列表快速滚动时布局抖动更少、measure/动画读到的是同一帧状态(0.85 动画后端与 0.88 事件派发重构都建立在这套结构上)。
顺带一句:组件侧的 View/Text 等核心组件在 0.87 Strict TS API 之后也走正式导出路径,深导入被禁,具体见 02-核心组件.md 与 12-常见陷阱与最佳实践.md。
把整条渲染路径压缩成一句话,方便记忆:React 算出差异,Fabric 维护影子树并在后台跑布局,UI 线程只做"把差异变成真实视图"这一件事。 你在 07-动画与手势.md 里看到的"原生驱动动画不碰 JS 线程"、在 10-调试测试与性能.md 里看到的"长列表掉帧先查 render 数量",本质都是这句话的推论。
Hermes:为什么 RN 需要一个自己的 JS 引擎
移动端 JS 引擎的考核指标和浏览器不一样:浏览器假设"应用只在页面会话里活几十秒、内存管够、JIT 预热划算";手机上的 App 要秒开、省内存、可被系统杀。通用引擎(V8/JSC)的 JIT 在冷启动场景反而是负担:解析源码、热身编译都要在启动路径上烧时间,而移动端 CPU/内存和 OS 限制(iOS 历史上禁止 JIT 执行可写页)让 JIT 收益打折。Hermes(0.70 起 RN 默认引擎)针对这条曲线做了取舍:
- 字节码预编译:构建期把 JS 编成 Hermes 字节码,启动时直接加载字节码,跳过解析和字节码生成的开销——这是 RN App 冷启动优化里性价比最高的一档;
- GC 与对象模型为小堆、短生命周期调优,峰值内存更低(低端安卓机收益明显);
- 内置 heap profiling 工具链,可导出堆快照排查内存泄漏(见
10-调试测试与性能.md); - 代价:不是完整 JIT,CPU 密集的纯 JS 计算比 V8/JSC 慢——这正好反过来强化了"重计算去原生侧"的架构原则。
把它串成一条构建流水线看就清楚了:
src/*.tsx ──Metro+Babel──► 单一 bundle(.js) ──hermesc──► 字节码 ──► 打进 ipa/apk
│
App 启动:加载字节码 ► 直接进 VM 执行(跳过解析/JIT 热身) ◄─────────┘浏览器里"下载 JS → 解析 → JIT 热身 → 才跑满速"的启动曲线,在 RN 里被整段消掉了。字节码是"一等产物"之后还衍生出更细的优化空间——Expo SDK 56 起默认开启 Hermes 字节码 diffing(与更新体积相关,语境见 11-构建与发布.md)。
版本节奏(都在事实层面):Android 从 0.74 起用预编译 Hermes 产物(不用自己编译引擎源码);0.82 引入实验性 Hermes V1(opt-in);0.84 起 V1 成为默认。V1 是一次从编译器到 VM 的重写(社区称 “Static Hermes”),官方与社区报道的收益:启动约快 2–8%、部分基准 40%+、内存占用更低,并带 WebAssembly 支持——后者意味着将来可以在 RN 里跑 WASM 模块(比如移植的 C++ 库或第三方 SDK),给 09-原生模块与平台代码.md 的高性能方案多留了一扇门。
iOS 侧同样的预编译红利:0.84 起 iOS 默认使用预编译 Hermes 二进制,pod install 不再从源码编引擎(Swift 源码编译降级为可选项)——老项目升级时 CI 的 pod 步骤普遍能省几分钟。
那 hermesEnabled=false 现在还有什么意义?bare Android 模板的这个开关只剩"退回系统 JSC"的用途:你会失去字节码预编译(启动显著变慢)、失去堆剖析、调试方式也退回远程 JSC 那套,换来的只是"和浏览器一致的执行模型"。除非你在排查一个高度怀疑是引擎差异的疑难杂症,否则不要关它——0.88 下把它当成"恒为 true 的历史遗留字段"理解即可。
Metro:打包器、开发服务器与 Fast Refresh
Metro 是 RN 官方的 JavaScript bundler——一个"为移动平台定制的打包器 + 开发服务器",承担三件事:开发期按需出 bundle、生产期把 TS/JSX 编译并打成一个可放进原生包的静态产物、以及资源管线。
心智对照:Vite/esbuild 经验怎么迁移
写给大部分有前端经验的读者,对照着记最快:
| 维度 | Vite / esbuild | Metro |
|---|---|---|
| 开发服务器 | 基于原生 ESM,浏览器按需拉模块 | 请求驱动出 bundle,Expo Go/模拟器一次性拉整包 |
| 编译 | esbuild(Go)做转译 | Babel 转译(模板里 @react-native/babel-preset,0.88 为 0.88 版) |
| HMR | Vite 自带 HMR | Fast Refresh:保留组件 state 热更新(终端按键 r 重载) |
| 平台感知 | 基本没有 | 核心能力:Button.ios.tsx / Button.android.tsx 按平台自动解析,AssetResolver 按像素密度选图 |
| 目标产物 | 给浏览器的静态资源 | 给 iOS/Android 应用包的 main.jsbundle(Hermes 字节码) |
react-native/setup-env:模块初始化入口。0.88 弃用了深路径 'react-native/Libraries/Core/InitializeCore'(react-native/setup-env 是正式替身,见 12-常见陷阱与最佳实践.md 的迁移条目)。Metro 版本与 RN 版本强相关(0.88 要求 Metro >= 0.87.1),升级 RN 时模板里的 @react-native/metro-config 会跟着对齐,不要单独锁死 Metro 版本。
开发期到底发生了什么
把 npx expo start 之后的一次"打开 App 看首屏"拆开看:
- Metro 启动,从入口(Expo 是
expo-router/entry,bare 是index.js加AppRegistry.registerComponent)开始解析依赖图; - 设备/模拟器上的 Expo Go 或 development build 向 Metro 请求针对本平台的 bundle;Metro 增量转译 + 缓存,第一次全量慢一些,之后每次都是秒出;
- App 拿到 bundle 交给 Hermes 执行,React 从根组件开始渲染——此时你手机上跑的 JS 和电脑上的 Metro 之间维持着一条 WebSocket 通道;
- 你改一个文件保存,Metro 只重转译受影响的模块,经通道推给 App,React 按 Fast Refresh 协议替换组件、保留 state——这就是"改代码秒级见效"的全部机制。
理解到这一层,就同时理解了为什么 dev 模式的 bundle 又肥又慢(未压缩、带 source map、开着 React 开发期检查)而 release 构建要单独验证,以及为什么"dev 下一切流畅、release 才暴露卡顿"或反之都是正常现象。凡是性能结论,一律以 release 构建为准,见 10-调试测试与性能.md。
值得理解的三个细节
- Fast Refresh ≠ 热更新上线方案。 Fast Refresh 是开发期的模块替换,保留 React state;生产环境用户拿到的 OTA 更新是另一回事(Expo Updates / CodePush 那条线,见
11-构建与发布.md),两者在中文里都叫"热更新",面试和方案评审时务必分清。 - inline source maps。 Metro 可以在 bundle 内嵌 source map,React Native DevTools 的断点、Sources 面板直接吃这个。开发期默认开启;生产 bundle 通常不带(体积与源码泄露考虑),符号化堆栈用发布产物对应的 map,细节在
10-调试测试与性能.md与11-构建与发布.md。 - 缓存出问题时的通解是清缓存。 Metro 的 transform cache 脏了的症状是"明明改了代码 bundle 里还是旧的"“依赖装了却不识别”,统一用
npx expo start -c(或 bare 的--reset-cache)清掉重来,这一招解决三成"灵异"问题。
React Native DevTools:0.76 起的官方调试入口
d 键:Metro/Expo 终端里按 d,直接打开 React Native DevTools——基于 Chrome DevTools 打造、由官方维护的调试器,从 0.76 起成为标准工具。它取代了过去那套"Chrome 远程调试 + React Native Debugger 第三方壳 + Flipper 插件生态"的组合,能力上:
- Elements:检查原生组件树(不是 DOM 树),看 props、样式、组件层级;
- Sources:真正的断点调试,走 inline source maps;
- Console:直接跑 JS,访问 Hermes 引擎能力(包括堆快照);
- Profilers:React 组件渲染剖析(集成 React DevTools)与 Hermes 性能追踪(对应
0.86 DevTools 改进这条线)。
日常的最短路径长这样,值得背下来:
npx expo start(或 bare 的 Metro)起来,扫码连接 Expo Go / development build;- 终端按
d打开 DevTools(Expo 终端同样支持); - Sources 里找到你的 TS 文件打断点——断点命中的是源码而不是转译产物,前提是 bundle 内嵌了 source map;
- Console 里对当前运行时直接求值,配合内嵌的 React 组件面板检查 props 传递链。
对老读者的提醒:如果你的教程里还在教"打开 Chrome → inspect element → 粘贴 localhost:8081/debugger-ui",那是 Bridge 时代的化石流程,作废。调试的完整工作流(真机、release 符号化、性能 trace)见 10-调试测试与性能.md。
版本选择与升级节奏
写 package.json 时必须知道的配对
RN 对 React 是 peerDependency 强绑定,装错版本轻则 warning、重则运行时崩。主会话用 npm registry 实测的映射(本手册可信度最高的一张表):
| react-native | react(peer 实测) |
|---|---|
| 0.78 | 19.0 |
| 0.80 | 19.1 |
| 0.82 | 19.1.1 |
| 0.83 | 19.2.0 |
| 0.85 ~ 0.87 | 19.2.3 |
| 0.88 | 19.3.0 |
推荐写法是精确锁定(不带 ^),让 lockfile 成为唯一事实来源:
{
"dependencies": {
"react": "19.3.0",
"react-native": "0.88.0"
}
}(0.88 发布于 2026-10-15,GitHub tag,博客 10-19;仓库已从 facebook/react-native 迁移到 react/react-native——看到旧地址的教程/issue 链接时心里换算一下。)
升级节奏:追 Expo SDK 比追 RN 现实
RN 的发布节奏约 2 个月一版(0.80→0.88 即 2025-06→2026-10 这条线)。对绝大多数团队,逐个 RN 版本升级的运营成本高于收益,现实的做法是按 Expo SDK 大版本升——Expo 帮你把 RN、React、Metro、Babel、TS 配置的版本矩阵一次对齐:
| Expo SDK | RN | 备注 |
|---|---|---|
| 54(2025-09) | 0.81 | 最后支持旧架构的 SDK |
| 55(2026-01) | 0.83 / React 19.2 | 移除 Legacy Architecture;Expo Router v7 |
| 56(2026-05) | 0.85 | Hermes 字节码 diffing 默认;expo-modules-core Swift 重写 |
| 57(2026-06-30) | 0.86 | 小而专的发布,探索新 SDK 节奏 |
| 58(2026-10-14) | 0.88 | “built for iOS 27”;Strict TS 默认;Expo Observe |
bare 项目则按"一年升 23 个 RN 版本、一次不跨多个版本"的节奏走,每次升级先看 release blog 里的 Breaking Changes(0.86/0.88 都标榜 no breaking changes,但第三方库兼容才是升级的真正阻力,见 0.86 要求 >=20.19.4,0.87 起 Node 20 出局**,升 0.87/0.88 前先把 CI 的 Node 升到 22.13+。12-常见陷阱与最佳实践.md)。Node 口径也随版本变:**0.81
不论哪条路径,一次升级动作要成套做完,缺一项都会出现"本地好了 CI 红了"或反之:
- 改
react-native与react精确版本(按上面的配对表); - 跟模板对齐 devDeps:
@react-native/babel-preset、metro-config、typescript-config、eslint-config、jest-preset与 RN 同号; - bare 项目对照新版本模板 diff
android/、ios/(Gradle/AGP/CocoaPods 配置变更都藏在模板里,别只 bump npm 就完事);Expo 项目跑npx expo install --check并重新 prebuild 验证生成工程; - iOS 重跑
pod install、Android 清一次 Gradle 缓存构建; - release 构建冒烟:启动、导航、列表滚动、原生模块调用各过一遍,dev 通过不代表 release 通过。
RN 与 Expo 的关系:框架与运行时之外
00 章讲了两条路径怎么选,这里补一层"关系"的认知:
- React Native 是渲染运行时 + 核心组件库,由
reactorg 维护,发布节奏跟着 RN 版本走; - Expo 是建立在 RN 之上的完整工具链与生态:脚手架与 CLI(
create-expo-app/expo)、原生能力封装(expo-modules-core,SDK 56 起 Swift 重写,社区戏称 “Swift all the way down”)、路由层 expo-router(版本号从 v7 起与 SDK 号对齐,58 的包是 58.0.x)、以及可选项的云服务(EAS 构建/更新/崩溃上报——SDK 58 把 AppMetrics 并入 Expo Observe 做原生崩溃上报)。
两条边界信号值得留意:其一,RN 官方 0.88 bare 模板预装了 @react-native/new-app-screen——核心团队开始以独立 npm 包的形式发布"模板即依赖"的能力,未来会有更多这类小包出现,“RN 只有框架本体"的旧印象又淡一分;其二,Expo 与 RN 的耦合是单向对齐的(SDK 跟 RN 版本,RN 不感知 Expo),所以 Expo 项目升 SDK 等于搭 RN 升级的班车,bare 项目则要自己盯班车时刻表。
高频架构问答
用工程师之间问答的方式,把这一章容易含混的点钉死:
Q:现在选库还需要考虑"新架构兼容"吗?
A:0.82 移除旧架构之后,这个问题变成了"这个库还活着吗”。RN 0.88 里仍有 interop 兼容层(以及默认关闭的 RCT_REMOVE_LEGACY_MODULE_INTEROP/RCT_REMOVE_LEGACY_COMPONENT_INTEROP 编译开关,官方计划未来默认移除),但那只服务于极少数未迁移的存量库。选型时看仓库的最近发布时间和 issue 里新架构崩溃报告的响应,比看 README 是否标了"支持新架构"更真实。
Q:JS 代码在手机上是不是跑在一个浏览器里?
A:不是。跑在 Hermes(一个独立的 JS 引擎库,随应用分发)里,没有 DOM、没有 BOM、没有 window/document,localStorage 这类 Web API 全都不存在。你能用的 JS 能力是语言本身 + RN 从 react-native 包根导出的那套 API(0.87 起 Strict TS 已禁止深导入 react-native/Libraries/*,0.88 连 InitializeCore 深路径都正式弃用、改走 react-native/setup-env)。把"RN 里怎么访问 document"这类问题问出口之前,先记住这一条(12-常见陷阱与最佳实践.md 里这是常客)。
Q:Hermes 比 V8 慢,那 RN 性能是不是天生差一档? A:把两个数分开看:峰值计算吞吐上 Hermes(无完整 JIT)确实低于 V8;启动时间、内存、帧率稳定性上 Hermes 通常赢,因为移动端性能瓶颈几乎从来不是"算得快",而是"每 16ms 有没有活干不完"。你的 App 卡,99% 是 JS 线程某一帧做了太活或原生侧主线程被占,不是引擎峰值算力不够。真遇到计算密集,方案是下沉原生或试 WASM(0.84 起 Hermes 支持),而不是换引擎。
Q:布局到底在哪算的?会不会算布局卡 UI? A:Yoga 在 Fabric 的后台流程里算,结果同步给 UI 线程挂载,Bridge 时代"布局风暴打满主线程"的形态已经不存在。但注意:布局输入仍由 JS 提交的样式驱动,JS 没算完 props,后台线程就没得算——所以"卡 JS 导致画面不更新"依然成立,卡的不一定是"绘制",常是"数据"。
Q:RN 页面之间的原生生命周期怎么对应?
A:别用浏览器页面模型套。RN 一个进程一个 JS 上下文,“路由"大多是视图栈/容器切换而非文档导航;导航库(react-navigation / expo-router,见 04-导航.md)负责在正确的时机挂载/卸载组件。0.88 补上了 iOS SceneDelegate 生命周期支持,这是苹果近年对 App 生命周期提出的新要求,纯细节可先不管。
Q:新架构让包体积变大还是变小?
A:这不是它的设计目标,量级上互有胜负;体积优化的正确战场在 bundle 内容与原生依赖裁剪,见 11-构建与发布.md。
什么时候不该用 React Native
诚实的权衡比盲目的安利有用。下面几类需求出现时,先停下来评估:
- 重图形/游戏:持续高帧率的粒子、自绘引擎、3D 场景——RN 的"每帧同步一次视图树"模型不是为这个设计的。这类产品用 Unity/Unreal/Flutter(Impeller 画布)或纯原生 Metal/Vulkan,比在 RN 里硬塞渲染层省力得多。RN 里嵌个 Metal/OpenGL 视图做局部特效是另一回事——那属于原生模块的活(
09-原生模块与平台代码.md)。 - 重后台硬件/系统能力:长时间后台蓝牙采集、持续定位+传感器融合、深度系统集成(特殊音频路由、后台网络栈定制)。RN 不禁止这些(原生模块全能做),但一旦核心卖点在这,原生团队的语言和心智才是主场,RN 只是多出来的一层。
- 合规/安全要求纯原生栈:某些金融/政企场景要求消除 WebView、要求代码全链路可审计或禁止 JS 动态执行——RN 的字节码 bundle 本身是动态下发物,合规上未必过关。
- 极端包体要求:Hermes + RN runtime 的原生基座体积摆在那里,一个功能很小的工具类 App 用它不划算;Flutter 同样有引擎体积,这条对比下两边都不占优。
- 团队没有 JS/React 资产:全员原生团队做 RN 等于多养一套工具链和一种语言;反之全员前端团队做纯原生也是。RN 最大的红利是"复用现有 React 团队”,没有这个红利,框架选型回归团队现状本身。
还有一类常被误判成"不该用"的场景要单独澄清:往已有原生 App 里嵌 RN 页面(brownfield 集成)。这在技术上完全可行,代价是两套构建与发版流程并存的运维复杂度——评估点应该放在"团队能否同时养住两条流水线",而不是"RN 能不能嵌"。
反过来说,RN 的最佳射程很清晰:业务驱动、内容型、表单流、交易流程类的产品型 App,两端视觉/交互 80% 复用,需要随时接原生 SDK,且团队以 React 为主语言。 绝大多数"公司要做一个 App"的场景都在这个射程里。
使用场景
把本章的概念映射回你接下来要写的第一批代码,按"这个需求主要踩在哪条机制上"归类,是建立架构直觉最快的方式:
| 你要做的事 | 主要踩到的机制 | 延伸章节 |
|---|---|---|
| 列表秒开不卡顿 | 布局在后台线程、滚动在 UI 线程、数据加载在 JS 线程 | 02-核心组件.md、10-调试测试与性能.md |
| 复杂手势拖拽/动画 | 0.85 Animation Backend(C++,不经 JS) | 07-动画与手势.md |
| 调用设备能力/三方 SDK | TurboModule + Codegen 契约 | 09-原生模块与平台代码.md |
| 热修复 JS 逻辑 | Metro bundle + OTA 通道(≠ Fast Refresh) | 11-构建与发布.md |
| 排查启动慢 | Hermes 字节码预编译 + JSI 装配链路 | 10-调试测试与性能.md |
| 依赖装了就崩 | RN↔React peer 配对、新架构兼容性 | 本章版本节、12-常见陷阱与最佳实践.md |
下一章开始进入日常写代码的部分:02-核心组件.md,看新架构下 View/Text/ScrollView/FlatList 这一层实际能用什么、不能用什么。
最后留一个自测题帮你把本章过一遍:解释"为什么 onScroll 回调密集的页面容易掉帧,但滚动本身通常不卡"。答案就藏在两个线程模型里——滚动是 UI 线程上系统控件的自带能力,onScroll 却要把事件送回 JS 线程处理;掉的是后者的帧,不是前者的。能顺畅推出这个答案,本章的架构心智就算建立了。