文章

RN三线程模型深度解析

拆解 RN 的 JS 线程、Shadow 线程、UI 线程的分工协作机制,以及三线程模型下的性能陷阱与调优策略。

RN三线程模型深度解析

一句话概括

RN 的渲染流水线由三条线程接力完成——JS 线程管”该画什么”(状态 + diff),Shadow 线程管”画在哪”(Yoga 布局计算),UI 线程管”真的画”(原生 View 创建 + 屏幕绘制)。三条线能不能并行、会不会互相等,直接决定了 FPS。

核心知识点

1. 三条线程职责一览

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  JS Thread   │    │Shadow Thread │    │  UI Thread   │
│              │    │              │    │              │
│ • React Diff │    │ • 构建Shadow │    │ • 创建/更新  │
│ • setState   │    │   Tree       │    │   Native View│
│ • 事件处理   │    │ • Yoga 布局  │    │ • 触摸事件   │
│ • API 调用   │    │ • 树 Diff    │    │ • 屏幕绘制   │
└──────┬───────┘    └──────┬───────┘    └──────┬───────┘
       │                   │                   │
       │ JSI 同步调用 ─────→│                   │
       │                   │ 原子提交 ─────────→│
       │                   │                   │
       │←─── 触摸事件 ──────┴───────────── 来源 │

关键区别:旧架构没有独立 Shadow 线程——Yoga 布局计算在 UI 线程跑。这意味着布局计算和 UI 绘制互相抢占,一个重布局的页面直接拖死 UI。Fabric 把这拆开后,布局再怎么复杂也不影响滑动流畅度。

2. JS 线程:最常被阻塞的一环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ❌ JS 线程阻塞的经典场景
function HeavyList({ data }) {
  // 在 render 中做大量同步计算 —— JS 线程被占满
  const processed = data.map(item => {
    // 假设这里做了复杂的排序/过滤/转换,花了 100ms
    return expensiveTransform(item)
  })

  return <FlatList data={processed} renderItem={...} />
  // JS 线程被这 100ms 占用期间:
  // - 触摸事件积压不处理(点击没反应)
  // - 新帧的 setState 排不上队
  // - Bridge/JSI 的通信也被拖延
}

// ✅ 搬走耗时计算
import { InteractionManager } from 'react-native'

useEffect(() => {
  InteractionManager.runAfterInteractions(() => {
    const processed = data.map(expensiveTransform)
    setProcessed(processed)
  })
}, [data])
// InteractionManager 在触摸/动画结束后才执行

JS 线程跑在任何单线程 JS 引擎上(JSC/Hermes/V8),一旦有一个长任务占据事件循环超过 16ms,这一帧就废了。排查 RN 卡顿的第一件事永远是:看 JS 线程有没有长任务。

3. Shadow 线程:新架构的秘密武器

1
2
3
4
5
6
7
8
// ShadowNode:Fabric 布局的最小单元(纯 C++)
class ShadowNode {
  Props::Shared props_;         // 样式属性
  State::Shared state_;         // 组件状态(如 TextInput 的光标位置)
  ShadowNodeFamily::Shared family_;
  ShadowNodeList children_;
  // YogaNode 内嵌在内,布局计算直接调用 Yoga C API
};

Shadow 线程只做三件事:① 把 JS 传过来的组件树转成 Shadow Tree(C++ 结构)② 跑 Yoga 计算每个节点的位置和大小 ③ 对比新旧两棵树,生成 diff,打包成原子提交给 UI 线程。

这里面最精妙的是 “atomic commit” —— 不是每改一个属性就通知一次 UI 线程,而是一轮的变更全算完了一起发。UI 线程只需要醒来一次,处理一批连续的 View 操作。

4. UI 线程:最后一棒

UI 线程收到底层传来的 Shadow Tree diff 后做两件事:

1
2
3
4
5
6
7
8
9
10
11
// Android: Fabric 通过 ViewManager 更新原生 View
// 所有操作在 UI 线程的 Looper 上排队执行
mountingManager.scheduleTransaction(() -> {
  for (Mutation m : transaction.getMutations()) {
    switch (m.type) {
      case CREATE: viewManager.createView(...); break;
      case UPDATE: viewManager.updateProperties(...); break;
      case DELETE: viewManager.dropView(...); break;
    }
  }
});

如果 UI 线程被占久了(比如创建了 200 个复杂 View),触摸事件就无法分发——用户感觉”界面不动了”。这就是为什么 FlatList 一定要懒渲染(只创建可见区域的 View),减少 UI 线程单帧的负担。

5. 三条线的协作如何影响 FPS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
一帧 16ms 的理想分配(60FPS):
│ JS 4ms │ Shadow 3ms │ UI 4ms │ 空闲 5ms │

掉帧的真实分配:
│ JS 14ms │ Shadow 3ms │ UI 2ms │ ← 超过帧预算,丢帧!
  ↑ 一个昂贵的 setState 把 JS 线程拖住了

另一种掉帧:
│ JS 3ms │ Shadow 12ms │ UI 2ms │ ← Shadow 线程算 Yoga 太慢
  ↑ 1000 个节点的复杂布局

第三种掉帧:
│ JS 2ms │ Shadow 3ms │ UI 15ms │ ← UI 线程创建大量 View
  ↑ FlatList 没有懒渲染,一次性创建全部

排查方法论:先看 Perf Monitor 里 JS FPS 还是 UI FPS 掉,JS 线掉→优化 JS 逻辑;UI 线掉→优化 View 层级和数量。

其实你每天都在用

  1. InteractionManager.runAfterInteractions() 就是 JS 线程的”等一下”指令 — 把低优先级任务推迟到触摸/动画结束后,确保关键帧不被打断
  2. useNativeDriver: true 让动画完全绕开 JS 和 Shadow 线程 — 动画在 UI 线程原生驱动,JS 再怎么忙动画都不掉帧
  3. FlatList 的 removeClippedSubviews、windowSize 等参数 — 本质上是在控制 Shadow 线程的节点数和 UI 线程的 View 数量,三条线的减负策略全用上了
  4. React DevTools Profiler 的 commit 耗时 — 一个 commit 里面既有 JS diff 又有 Shadow 布局,超过 16ms 就掉帧
  5. Android Systrace 里能看到 reactNative.js_loader、fabric.shadow、Choreographer 三条时间线 — 卡顿排查就是在这些时间线之间找空白和重叠

常见误解(FAQ)

❌ 误区:「JS 线程死循环了,UI 线程也会死」 不会。UI 线程完全独立运行——屏幕刷新、原生动画、系统弹窗都照样跑。但是,触摸事件虽然由 UI 线程捕获,处理需要 JS 线程参与(执行 onPress 等),JS 线程卡死会导致”触摸了但没反应”。另外,如果用了不带 useNativeDriver 的 JS 驱动动画,动画也会停止。

❌ 误区:「Shadow 线程是万能加速器,引入后所有布局都变快了」 Shadow 线程只是把布局计算从 UI 线程”搬走”了,布局计算本身的时间没变(Yoga 还是那个 Yoga)。好处是布局不再阻塞 UI,但如果布局本身就是瓶颈(比如 1000 个 flex 节点),该慢还是慢,只是慢在 Shadow 线程上了。解决思路是减少节点数,不是指望线程分离。

❌ 误区:「三线程模型下所有操作都并行执行,所以一定比单线程快」 线程间有严格的依赖顺序:JS 线程算完 diff → Shadow 线程才能布局 → UI 线程才能渲染。这是串行依赖,不是并行。真正并行的是不同帧之间——第 N 帧的 JS 计算和第 N-1 帧的 UI 渲染可以同时进行(类似 CPU 流水线)。

❌ 误区:「Debug 模式下三线程模型也一样」 Debug 模式下 JS 跑在电脑 Chrome 里,JS 线程和 Shadow 线程之间还要加一层 WebSocket(真机↔Chrome),通信延迟比在真机上直接跑 JS 引擎大得多。Perf Monitor 在 Debug 模式下的数据没有任何参考价值。

一句话总结

三线程模型的本质是”前端渲染流水线的工业化”——JS 线程是创意部门画设计图,Shadow 线程是工程部算施工尺寸,UI 线程是工人上墙——任何一个部门摸鱼,项目都按时交不了差。

本文由作者按照 CC BY 4.0 进行授权