React Fiber 架构与并发模式

困难 🔴React 生态
7 个标签
预计阅读时间:67 分钟
ReactFiber并发调度时间分片LaneSuspense

React Fiber 架构与并发模式

React Fiber 是 React 16 引入的全新协调引擎,它彻底重构了 React 的核心算法,为 React 带来了并发渲染能力。Fiber 的设计目标是解决大型 React 应用中更新卡顿的问题,实现可中断的渲染过程,让高优先级的用户交互能够快速响应。

如果把旧版 React(Stack Reconciler,栈协调器)比作一个"一口气跑完全程的马拉松选手",那么 Fiber 就是一个"每跑一小段就抬头看看有没有紧急任务的选手"。前者一旦开跑就无法停下,中途用户点击、输入统统被阻塞;后者可以随时暂停、让路、再回来接着跑。这个"可以随时暂停和恢复"的能力,正是整个并发渲染的根基。

为什么需要 Fiber

浏览器主线程是单线程的:JavaScript 执行、样式计算、布局、绘制、用户事件响应都挤在同一条线程上。主流显示器刷新率是 60Hz,也就是每 16.6ms 需要产生一帧。如果一次 JavaScript 任务(比如 React 渲染一棵很大的组件树)占用主线程超过这个时间,浏览器就来不及绘制下一帧,用户就会看到"掉帧"、输入框打字延迟、点击没反应等卡顿现象。

旧版 React 的协调过程是同步且不可中断的递归。一旦 setState 触发更新,React 会一路递归比对整棵虚拟 DOM 树,直到全部完成才交还主线程。假设一次更新需要处理 3000 个组件、耗时 300ms,那么这 300ms 内页面完全冻结,用户任何操作都得不到响应。

Fiber 的解决思路是:把一次大的渲染任务拆成许多小单元,每做完一个单元就检查"我是否已经占用主线程太久了?浏览器有没有更紧急的事要做?",如果有就暂停、把控制权还给浏览器,等浏览器空闲再回来继续。

| 维度 | Stack Reconciler(React 15) | Fiber Reconciler(React 16+) |

| --- | --- | --- |

| 遍历方式 | 递归调用栈 | 链表 + 循环,可保存进度 |

| 是否可中断 | 不可中断 | 可中断、可恢复、可丢弃 |

| 优先级 | 无优先级概念 | 多优先级(Lane 模型) |

| 长任务表现 | 阻塞主线程,页面卡死 | 时间分片,交互优先响应 |

| 数据结构 | 虚拟 DOM 树 | Fiber 节点链表树(双缓存) |

Fiber 核心概念深度解析

Fiber 节点 - 核心数据结构:

Fiber 节点是 Fiber 架构的核心数据结构,每个 React 元素对应一个 Fiber 节点。Fiber 节点包含了组件的所有信息:type(组件类型)、key(列表标识)、props(属性)、state(状态)、effectTag(副作用标记)等。Fiber 节点通过 return、child、sibling 三个指针形成链表树结构,return 指向父节点,child 指向第一个子节点,sibling 指向下一个兄弟节点。这种链表结构使得遍历可以在任意节点暂停和恢复,是实现可中断渲染的基础。

为什么链表能实现"可中断"而递归不能?因为递归的进度保存在 JavaScript 的调用栈里,一旦你想暂停,就必须一层层退栈——退栈之后进度就丢了。而链表把父子、兄弟关系显式地存在节点的指针里,React 只需要记住"我现在处理到哪个 Fiber 节点"这一个引用,随时可以停下,下次拿着这个引用就能接着走。这相当于把"隐式的调用栈"改写成了"显式的、可持久化的数据结构"。

双缓存技术 - 无闪烁更新:

双缓存技术是 Fiber 架构的关键优化,React 同时维护两棵 Fiber 树:current 树(当前屏幕显示的树)和 workInProgress 树(正在构建的新树)。当 workInProgress 树构建完成后,React 只需要交换 rootFiber 的 current 指针,就能完成更新。这种技术避免了逐个节点更新导致的页面闪烁,同时支持回滚操作。双缓存还使得 React 能够在内存中完成所有计算后再一次性提交到 DOM,提高了渲染效率。

双缓存的类比就是显卡的"双缓冲区":屏幕正在显示 A 缓冲区的画面,GPU 在后台往 B 缓冲区画下一帧,画完后直接切换指针让屏幕显示 B,观众永远看不到"画到一半"的中间态。current 和 workInProgress 两棵树通过 alternate 指针相互连接,复用节点、减少内存分配。

调度器 - 智能任务管理:

调度器(Scheduler)负责管理任务的优先级和执行时机。React 将任务分为不同优先级:Immediate(立即执行,如用户输入)、UserBlocking(用户阻塞级别,约 250ms 超时)、Normal(普通级别,约 5s 超时)、Low(低优先级,约 10s)、Idle(空闲时执行,永不超时)。调度器使用时间分片技术,将大任务拆分为小任务,每个任务执行一段时间后检查是否需要让出主线程,避免阻塞用户交互。高优先级任务可以中断正在执行的低优先级任务,确保用户交互的流畅性。

渲染的两大阶段:Render 与 Commit

Fiber 把一次更新明确划分为两个阶段,理解这个划分是理解并发模式的关键:

Render 阶段(协调阶段):构建 workInProgress 树、执行 diff、标记副作用(flags)。这个阶段是可中断的,可能被执行、暂停、丢弃、重新执行——所以这个阶段里的代码(比如函数组件体、getDerivedStateFromProps)必须是纯函数,不能有副作用,否则一次更新被打断重来会导致副作用执行多次。
Commit 阶段(提交阶段):把 Render 阶段计算出的变更一次性同步应用到真实 DOM,执行 DOM 操作、调用 useLayoutEffect、更新 ref。这个阶段是同步、不可中断的,必须一口气完成,保证用户看到的永远是一致的完整界面。

这也解释了 React 18 里"函数组件在开发环境 StrictMode 下会执行两次"的现象——React 故意重复调用来帮你发现不纯的副作用。

时间分片工作原理

时间分片(Time Slicing)的底层依赖于一个"是否该让出主线程"的判断。早期 React 尝试用浏览器的 requestIdleCallback,但它触发不稳定、频率低,React 团队最终自己实现了基于 MessageChannel 的调度循环。核心逻辑可以简化为下面这段伪代码。

javascriptCode
// React Scheduler 时间分片核心逻辑(简化版)
let deadline = 0;
const frameYieldMs = 5; // React 默认每个时间片约 5ms

function shouldYield() {
  const now = performance.now();
  // 当前时间片用完了,应该让出主线程
  return now >= deadline;
}

function workLoop() {
  // 每次进入循环重新计算这一片的截止时间
  deadline = performance.now() + frameYieldMs;

  // 只要还有工作、并且时间片没用完,就一直做
  while (nextUnitOfWork !== null && !shouldYield()) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  if (nextUnitOfWork !== null) {
    // 活没干完但时间到了:注册下一次回调,先把主线程还给浏览器
    scheduleCallback(workLoop);
  } else {
    // 全部完成,进入 commit 阶段
    commitRoot();
  }
}

// 处理单个 Fiber 节点,返回下一个要处理的节点
function performUnitOfWork(fiber) {
  // 1. beginWork:处理当前节点,创建子 Fiber
  const next = beginWork(fiber);
  if (next) return next; // 有子节点,深度优先向下

  // 2. 没有子节点,向上回溯,处理兄弟节点
  let node = fiber;
  while (node) {
    completeWork(node); // 完成当前节点,收集副作用
    if (node.sibling) return node.sibling;
    node = node.return;
  }
  return null;
}

Fiber 节点结构示意

javascriptCode
// Fiber 节点结构示意(真实字段远比这多,这里挑核心的)
const fiber = {
  type: 'div',           // 元素类型:字符串标签 / 函数组件 / 类组件
  key: null,             // 列表 key,用于 diff 时精准匹配
  props: {               // 属性
    className: 'container',
    children: []
  },
  stateNode: null,       // 对应的真实 DOM 节点或组件实例

  // Fiber 树结构(三个指针构成链表树)
  return: parentFiber,    // 父节点
  child: firstChildFiber, // 第一个子节点
  sibling: nextFiber,     // 下一个兄弟节点

  // 双缓存
  alternate: currentFiber, // 指向另一棵树中对应的节点,复用内存

  // 状态和副作用
  memoizedState: null,   // 类组件为 state,函数组件为 Hooks 链表
  memoizedProps: null,   // 上一次的 props
  pendingProps: props,   // 本次待处理的 props
  effectTag: 0,          // 旧版副作用标记(Placement, Update, Deletion 等)
  flags: 0,              // 新版 React 使用 flags 表示副作用
  lanes: 0,              // 优先级标记(Lane 模型,用位运算表示)
};

Lane 优先级模型

React 17 之后用 Lane(车道)模型替代了早期的 expirationTime 优先级。Lane 用一个 31 位的二进制掩码表示优先级,每一位代表一条"车道",位越靠右优先级越高。用位运算可以高效地合并、判断、取最高优先级,这比数字比较灵活得多——它能同时表达"一批更新"而不只是"一个更新"。

javascriptCode
// Lane 模型示意(用二进制位表示优先级)
const SyncLane =              0b0000000000000000000000000000001; // 最高:同步
const InputContinuousLane =   0b0000000000000000000000000000100; // 连续输入
const DefaultLane =           0b0000000000000000000000000010000; // 默认
const TransitionLane =        0b0000000000000000000000001000000; // 过渡
const IdleLane =              0b0100000000000000000000000000000; // 空闲

// 合并多条车道(一批更新可能属于多个优先级)
const pendingLanes = SyncLane | DefaultLane;

// 取最高优先级车道:分离出最低位的 1(值最小 = 优先级最高)
function getHighestPriorityLane(lanes) {
  return lanes & -lanes; // 位运算技巧,O(1) 取最高优先级
}

// 判断某批更新是否包含某条车道
function includesLane(set, lane) {
  return (set & lane) !== 0;
}

并发特性代码示例

javascriptCode
// useTransition 使用示例:区分"紧急更新"与"非紧急更新"
function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e) => {
    const value = e.target.value;

    // 立即更新输入框(高优先级,紧急更新)
    setQuery(value);

    // 将搜索结果更新标记为低优先级(过渡更新,可被打断)
    startTransition(() => {
      const filtered = largeDataList.filter(item =>
        item.name.toLowerCase().includes(value.toLowerCase())
      );
      setResults(filtered);
    });
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleSearch}
        placeholder="Search..."
      />
      {isPending && <span>Searching...</span>}
      <ResultList results={results} />
    </div>
  );
}

// useDeferredValue 使用示例:延迟派生值的更新
function DeferredList({ query }) {
  // deferredQuery 会"滞后"于 query,输入时先用旧值渲染,保证输入流畅
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(() => {
    return largeDataList.filter(item =>
      item.name.toLowerCase().includes(deferredQuery.toLowerCase())
    );
  }, [deferredQuery]);

  // 通过对比判断是否处于"陈旧"状态,可以给个视觉提示
  const isStale = query !== deferredQuery;

  return (
    <div style={{ opacity: isStale ? 0.6 : 1 }}>
      {results.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}
javascriptCode
// Suspense 数据获取示例:嵌套 Suspense 实现细粒度加载
function ProfilePage() {
  return (
    <Suspense fallback={<Loading />}>
      <ProfileDetails />
      {/* 内层 Suspense 独立控制照片区域的加载状态 */}
      <Suspense fallback={<PhotosSkeleton />}>
        <ProfilePhotos />
      </Suspense>
    </Suspense>
  );
}

// 配合 Suspense 的资源封装(简化版,生产环境请用 React Query / use 等成熟方案)
function fetchProfileData(id) {
  let status = 'pending';
  let result;
  const suspender = fetchUser(id).then(
    (data) => {
      status = 'success';
      result = data;
    },
    (error) => {
      status = 'error';
      result = error;
    }
  );

  return {
    read() {
      if (status === 'pending') throw suspender; // 抛出 Promise,触发 Suspense
      if (status === 'error') throw result;      // 抛出错误,触发错误边界
      return result;
    }
  };
}
javascriptCode
// React 18 启用并发能力:使用 createRoot 而非 legacy 的 ReactDOM.render
import { createRoot } from 'react-dom/client';

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);

// 自动批处理示例(React 18 的默认行为)
function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // 同步事件里,React 17 和 18 都会批处理这两次更新
  const handleReset = () => {
    setFirstName('');
    setLastName('');
    // 只触发一次重新渲染
  };

  // 关键区别:异步回调里
  const handleAsyncReset = async () => {
    await fetchData();
    setFirstName('');
    setLastName('');
    // React 17:触发两次渲染;React 18:自动批处理,只触发一次
  };

  // 如果确实需要立即渲染,用 flushSync 退出批处理
  const handleImmediate = () => {
    flushSync(() => setFirstName('A')); // 立即渲染
    flushSync(() => setLastName('B'));  // 再次立即渲染
  };

  return (
    <form>
      <input value={firstName} onChange={e => setFirstName(e.target.value)} />
      <input value={lastName} onChange={e => setLastName(e.target.value)} />
      <button type="button" onClick={handleReset}>Reset</button>
    </form>
  );
}

真实案例:万级列表搜索过滤

设想一个后台管理系统,需要在一个 20000 行的表格上做实时搜索。在没有并发特性时,用户每敲一个字符,React 就要同步过滤并重渲染 20000 行,主线程被占用约 200~400ms,导致输入框严重卡顿——你打字"react",屏幕过好几秒才把五个字母全显示出来。

引入 useTransition 后,输入框的 setQuery 是紧急更新立即生效,而 setResults 的重渲染被标记为过渡更新,可以被后续输入打断。用户连续快速输入时,中间那些"过时"的过滤计算会被 React 直接丢弃,只渲染最后一次结果。实测输入延迟从数百毫秒降到几乎无感,isPending 还能顺带显示一个 loading 态。

javascriptCode
// 万级列表搜索:并发特性前后对比
function BigTableSearch({ rows }) { // rows.length ≈ 20000
  const [keyword, setKeyword] = useState('');
  const [visibleRows, setVisibleRows] = useState(rows);
  const [isPending, startTransition] = useTransition();

  const onInput = (e) => {
    const value = e.target.value;
    setKeyword(value); // 紧急:输入框立即回显

    startTransition(() => { // 非紧急:大列表过滤,可被打断
      setVisibleRows(
        rows.filter(r => r.name.toLowerCase().includes(value.toLowerCase()))
      );
    });
  };

  return (
    <>
      <input value={keyword} onChange={onInput} />
      {isPending && <Spinner />}
      <VirtualTable data={visibleRows} />
    </>
  );
}

性能数据对比

| 场景(20000 行列表) | React 17 同步渲染 | React 18 + useTransition |

| --- | --- | --- |

| 单次输入主线程占用 | 约 200~400ms | 输入即时响应(分片 5ms/片) |

| 输入延迟体感 | 明显卡顿、掉字 | 几乎无感 |

| 快速连续输入 | 每次都完整重算 | 中间结果被丢弃,只算最后一次 |

| 是否阻塞点击/滚动 | 阻塞 | 不阻塞,可被高优先级打断 |

> 注:以上为典型量级的经验数据,实际数值取决于设备性能、列表复杂度和渲染成本。核心结论是:并发模式不会让"总计算量"变少,但它把计算切碎、按优先级重排,从而让交互延迟大幅下降。

常见坑

在 Render 阶段写副作用:因为 Render 阶段可能被打断重来,在函数组件体内直接发请求、改全局变量会导致重复执行。副作用必须放进 useEffect/useLayoutEffect。
误以为 useTransition 能让计算变快:它只改变优先级,不减少工作量。真正的重计算仍需配合虚拟列表、useMemo、Web Worker 等手段。
startTransition 里放紧急更新:把输入框回显这类必须立即生效的更新放进 startTransition,会导致输入延迟,恰好用反了。
Suspense fallback 闪烁:数据很快返回时,fallback 一闪而过反而更难受,可用 useDeferredValue 或 startTransition 保留旧内容避免闪烁。
仍在用 ReactDOM.render:不切换到 createRoot,所有并发特性(自动批处理、Transition)都不会生效,等于白写。
依赖 StrictMode 双调用误判 bug:开发环境组件执行两次是故意的,不要为此写"去重"逻辑,生产环境只执行一次。

最佳实践

用 createRoot 挂载应用,确保并发能力开启。
明确区分"紧急更新"(输入、点击、拖拽)和"非紧急更新"(搜索结果、图表重算),后者用 useTransition / useDeferredValue 降级。
保持 Render 阶段纯净:组件体、useMemo 回调、reducer 都不要有副作用。
大列表叠加虚拟滚动(react-window / react-virtualized),从根本上减少节点数量。
用 React DevTools Profiler 观察渲染耗时和被中断情况,用数据驱动优化。
Suspense 与错误边界(Error Boundary)搭配使用,分别处理 loading 与 error 两种异常态。

Fiber 节点字段逐个详解

前面给出了 Fiber 节点的核心字段示意,但要真正读懂 React 源码,必须理解每个字段在协调过程中扮演的角色。下面按"静态描述"、"树结构"、"工作数据"、"副作用"、"调度"五类逐一拆解,并配合真实源码中 createFiber 的初始化过程说明。

静态描述类字段

这一类字段在 Fiber 创建后基本不变,用来描述"这个节点是什么"。

tag(WorkTag):一个数字枚举,标识 Fiber 的种类。React 内部用它做 switch 分发,决定 beginWork/completeWork 走哪条分支。常见取值:FunctionComponent = 0、ClassComponent = 1、HostRoot = 3(根节点)、HostComponent = 5(原生 DOM 如 div)、HostText = 6(文本节点)、Fragment = 7、ContextProvider = 10、ForwardRef = 11、SuspenseComponent = 13、MemoComponent = 14、LazyComponent = 16、OffscreenComponent = 22。看到 tag 就知道该节点如何被处理。
key:列表 diff 用的稳定标识。React 用 key 在同层子节点间建立"身份映射",从而在顺序变化时复用而非销毁重建。key 不参与渲染,只服务于协调算法。
elementType 与 type:elementType 是原始的组件类型;type 通常与 elementType 相同,但在 React.lazy、React.memo 解析后会被替换成解析后的真实类型。二者分开是为了热更新(HMR)和 lazy 解析场景。
typescriptCode
// 简化的 WorkTag 枚举(对应 react-reconciler/src/ReactWorkTags.js)
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const HostRoot = 3;       // 应用挂载的根 Fiber
export const HostComponent = 5;  // 原生标签,如 <div>
export const HostText = 6;       // 纯文本
export const Fragment = 7;
export const SuspenseComponent = 13;
export const OffscreenComponent = 22; // Offscreen / Activity 的底层实现

树结构类字段

child:指向第一个子 Fiber。注意只指向"第一个",其余子节点靠 sibling 串联。
sibling:指向右边的下一个兄弟 Fiber,最后一个兄弟的 sibling 为 null。
return:指向父 Fiber(叫 return 而不是 parent,是因为它同时承担"处理完当前子树后应该返回到哪里"的语义,类似函数调用的返回地址)。
index:当前 Fiber 在兄弟中的下标,diff 判断节点是否需要移动时用到。

这三个指针 child / sibling / return 构成的不是普通的多叉树,而是一棵可以被"深度优先线性遍历"的链表树。遍历规则固定:能往下就走 child,不能往下就走 sibling,都不行就沿 return 回溯再找父级的 sibling。整个 workLoop 就是在这三个指针上循环。

typescriptCode
// 深度优先遍历一棵 Fiber 树(等价于 React 的遍历顺序)
function traverse(root: Fiber) {
  let node: Fiber | null = root;
  while (node !== null) {
    visit(node);
    if (node.child) {
      node = node.child;          // 优先向下
      continue;
    }
    while (node && !node.sibling) {
      node = node.return;         // 回溯到有兄弟的祖先
    }
    node = node ? node.sibling : null; // 走兄弟
  }
}

工作数据类字段

stateNode:指向该 Fiber 关联的"实体"。对 HostComponent 是真实 DOM 节点,对 ClassComponent 是组件实例,对 HostRoot 是 FiberRootNode。函数组件没有实例,stateNode 为 null。
pendingProps:本次更新开始时传入的新 props,beginWork 时读它。
memoizedProps:上一次渲染完成后固化下来的 props。beginWork 里会比较 pendingProps 与 memoizedProps 来决定能否命中 bailout(跳过更新)。
memoizedState:类组件里是 this.state;函数组件里是 Hooks 链表的头指针——每次调用 useState/useEffect 都是在这条链表上按顺序取值,这正是"Hooks 不能写在条件语句里"的根本原因。
updateQueue:待处理的更新队列。类组件是 setState 产生的 Update 环状链表;函数组件里挂 useEffect/useLayoutEffect 的副作用链表。
typescriptCode
// 函数组件的 memoizedState 是一条 Hook 链表
type Hook = {
  memoizedState: any;   // 当前 hook 存的值,如 useState 的 state
  baseState: any;
  queue: any;           // 更新队列(dispatch 往这里塞)
  next: Hook | null;    // 指向下一个 hook,形成链表
};

// 之所以 Hooks 必须固定顺序调用:React 靠"第几个被调用"来定位对应 Hook
// 条件语句里写 Hook 会让链表错位,取到别的 hook 的值

副作用与调度类字段

flags(旧名 effectTag):位掩码,标记该 Fiber 在 commit 阶段需要执行哪些 DOM 副作用。常见值:Placement = 0b0010(插入)、Update = 0b0100(更新)、Deletion(删除)、Ref = 0b0000001000000000(更新 ref)、Passive(有 useEffect)。多个副作用用按位或叠加。
subtreeFlags:React 18 引入,汇总"子树里是否存在需要 commit 的副作用"。commit 阶段靠它做剪枝——如果一整棵子树的 subtreeFlags 为 0,直接跳过遍历,避免旧版遍历整条 effectList 的开销。
lanes 与 childLanes:lanes 是该 Fiber 自身待处理更新的优先级车道集合;childLanes 汇总子树的车道。调度时靠 childLanes 快速判断某棵子树里是否有对应优先级的活要干。
alternate:指向双缓存中"对面那棵树"的对应 Fiber。current 的 alternate 是 workInProgress,反之亦然。
typescriptCode
// 常见 flags(对应 react-reconciler/src/ReactFiberFlags.js,值为简化示意)
export const NoFlags   = 0b0000000000000000000000000000;
export const Placement = 0b0000000000000000000000000010; // 需要插入/移动
export const Update    = 0b0000000000000000000000000100; // 需要更新属性
export const Deletion  = 0b0000000000000000000000001000; // 需要删除
export const Ref       = 0b0000000000000001000000000000; // 需要处理 ref
export const Passive   = 0b0000000000000100000000000000; // 有 useEffect

// commit 阶段判断:这个 Fiber 是否需要处理 ref
function needCommitRef(fiber: { flags: number }) {
  return (fiber.flags & Ref) !== NoFlags;
}

Render 阶段:beginWork 与 completeWork

Render 阶段的核心是 performUnitOfWork 对每个 Fiber 执行"递"(beginWork)和"归"(completeWork)两个动作。整体是一次深度优先遍历:"递"阶段自顶向下创建子 Fiber,"归"阶段自底向上创建 DOM 并收集副作用。

beginWork:向下创建子 Fiber

beginWork 接收 current(旧 Fiber)和 workInProgress(新 Fiber),根据 tag 分发到不同处理函数,最终目标是返回 workInProgress.child(下一个工作单元)。它最重要的优化是 bailout:如果 props 没变、context 没变、且该 Fiber 及其子树没有待处理的 lanes,就直接复用旧子树,返回 null 或克隆的子节点,跳过整棵子树的重渲染。这正是 React.memo、PureComponent 生效的底层机制。

typescriptCode
// beginWork 的骨架(大幅简化)
function beginWork(current: Fiber | null, workInProgress: Fiber, renderLanes: number) {
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    // props 与 context 都没变,且这棵子树没有待处理优先级 -> 尝试 bailout
    if (oldProps === newProps && !hasContextChanged() &&
        !includesSomeLane(renderLanes, workInProgress.lanes)) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  }
  // 按 tag 分发
  switch (workInProgress.tag) {
    case FunctionComponent:
      return updateFunctionComponent(current, workInProgress, renderLanes);
    case ClassComponent:
      return updateClassComponent(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    // ... 其余 tag
  }
}

对函数组件而言,updateFunctionComponent 会调用 renderWithHooks,真正执行你写的组件函数体,产出新的 React 元素(children),再交给 reconcileChildren 生成子 Fiber。

completeWork:向上创建 DOM 与冒泡副作用

当一个 Fiber 没有 child(到达叶子)时,进入 completeWork。它自底向上做三件事:为 HostComponent 创建真实 DOM 节点(此时还在内存中,未挂载)、把子 DOM append 到父 DOM、计算属性 diff 并打上 Update 标记,最后把自己的 flags 冒泡合并进父节点的 subtreeFlags。

typescriptCode
// completeWork 的骨架(简化)
function completeWork(current: Fiber | null, workInProgress: Fiber) {
  const newProps = workInProgress.pendingProps;
  switch (workInProgress.tag) {
    case HostComponent: {
      if (current !== null && workInProgress.stateNode != null) {
        // 更新:diff 新旧 props,算出需要变更的属性数组,打 Update 标记
        updateHostComponent(current, workInProgress, newProps);
      } else {
        // 首次:创建真实 DOM,把已完成的子 DOM 追加进来
        const instance = createInstance(workInProgress.type, newProps);
        appendAllChildren(instance, workInProgress);
        workInProgress.stateNode = instance;
      }
      bubbleProperties(workInProgress); // 冒泡 subtreeFlags
      return null;
    }
    // ... HostText / 其它
  }
}

之所以在 Render 阶段就把 DOM 创建好放在内存里,是为了让 Commit 阶段只做"挂载/属性更新"这类最小同步操作,尽量缩短不可中断的 commit 时长。

reconcileChildren 与 diff 算法

reconcileChildren 是协调的心脏,负责把新的 React 元素与旧 Fiber 对比,产出新的子 Fiber 并打上 Placement/Deletion 等标记。React 的 diff 基于三条前提假设把理论上 O(n^3) 的树 diff 降到 O(n):不同类型的元素产出不同的树(type 变了直接销毁重建)、同层比较不跨层移动、开发者用 key 提示哪些子元素稳定。

对多节点列表,diff 采用两轮遍历

1.第一轮:新旧列表按下标同步向后走,key 和 type 都相同就复用,一旦遇到不匹配立即跳出。这一轮专门优化"仅更新、不移动"的高频场景。
2.第二轮:把剩余旧 Fiber 存进一个 key -> Fiber 的 Map,然后遍历剩余新元素,用 key 去 Map 里找可复用节点。用一个 lastPlacedIndex 记录"最后一个不需要移动的旧节点下标",据此判断某个复用节点是否需要打 Placement(右移)标记。
typescriptCode
// 多节点 diff 第二轮的核心:用 Map 做 key 匹配,lastPlacedIndex 判断移动
function reconcileByKey(existing: Map<string, Fiber>, newChildren: Element[]) {
  let lastPlacedIndex = 0;
  for (let i = 0; i < newChildren.length; i++) {
    const el = newChildren[i];
    const matched = existing.get(el.key);
    if (matched) {
      if (matched.index < lastPlacedIndex) {
        markPlacement(el);              // 旧位置在已放置点左边 -> 需要右移
      } else {
        lastPlacedIndex = matched.index; // 保持相对顺序,无需移动
      }
      existing.delete(el.key);
    } else {
      markPlacement(el);               // 新增节点
    }
  }
  existing.forEach((fiber) => markDeletion(fiber)); // 剩下的旧节点删除
}

这也解释了列表 key 用 index 的著名陷阱:在头部插入元素时,所有元素的 index 都变了,diff 认为几乎全部节点都需要更新甚至移动,既丢失了内部状态又损失了性能。用稳定唯一的 id 作为 key 才能让 diff 精准复用。

双缓存树切换的完整生命周期

双缓存不是抽象概念,它有明确的生命周期,理解它才能明白"为什么更新是原子的、可回滚的"。

mount 首次渲染

首次渲染时只有一个 FiberRootNode(整个应用的根,注意它不是 Fiber,而是持有 current 指针的容器)和一个 HostRoot Fiber。此时 current 树只有根节点,React 以它为蓝本创建 workInProgress 树,自顶向下把整棵应用构建出来。构建完成、commit 后,FiberRootNode.current 从旧根指向新根,屏幕首次呈现。

update 更新渲染

后续每次更新,React 都基于 current 树克隆出一棵新的 workInProgress 树。克隆时靠 alternate 指针复用节点对象:如果 current 上某节点存在 alternate,就复用这个已有对象(重置属性),否则新建。这样两棵树在多次更新中来回复用,避免频繁 GC。

typescriptCode
// createWorkInProgress:从 current 派生 workInProgress,靠 alternate 复用
function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let wip = current.alternate;
  if (wip === null) {
    // 首次:新建一个 Fiber,双向挂 alternate
    wip = createFiber(current.tag, pendingProps, current.key);
    wip.stateNode = current.stateNode;
    wip.alternate = current;
    current.alternate = wip;
  } else {
    // 复用:重置属性,避免重新分配内存
    wip.pendingProps = pendingProps;
    wip.flags = NoFlags;
    wip.subtreeFlags = NoFlags;
  }
  wip.child = current.child;
  wip.memoizedProps = current.memoizedProps;
  wip.memoizedState = current.memoizedState;
  wip.lanes = current.lanes;
  return wip;
}

切换的原子性

关键点在于:workInProgress 树在内存中完整构建期间,屏幕上显示的始终是 current 树,用户看到的界面稳定不变。只有当 workInProgress 树完全 commit 完毕,React 才执行 root.current = finishedWork 这一行赋值——这一步是同步的、不可分割的。因此要么用户看到旧界面、要么看到完整的新界面,永远不会看到"半成品"。而如果 Render 阶段被高优先级任务打断,未完成的 workInProgress 树可以被直接丢弃,current 树毫发无损,这就是可中断渲染安全的根本保障。

Commit 阶段的三个子阶段

Commit 阶段是同步不可中断的,React 把它细分为三个子阶段依次执行,顺序不能乱,每个子阶段解决不同问题。

before mutation(变更前)

在真正操作 DOM 之前执行。主要工作:对类组件调用 getSnapshotBeforeUpdate,捕获变更前的 DOM 信息(比如滚动位置);调度 useEffect 的异步执行(useEffect 的回调不在这里执行,而是被 Scheduler 以 NormalPriority 异步调度,等浏览器绘制后才跑)。此阶段 DOM 还是旧的。

mutation(变更中)

真正修改真实 DOM 的阶段。根据每个 Fiber 的 flags 执行对应操作:Placement 插入节点、Update 更新属性、Deletion 删除节点。在这一阶段,旧的 ref 会先被置空(detach),useLayoutEffect 的销毁函数在这里同步执行(对应上一次 effect 的清理)。此阶段执行完,真实 DOM 已经是新的了。

layout(布局后)

DOM 已更新完成,此阶段读取更新后的 DOM 是安全的。类组件的 componentDidMount / componentDidUpdate 在这里同步调用;useLayoutEffect 的创建函数在这里同步执行;新的 ref 在这里被赋值(attach)。因为整个 layout 阶段与 mutation 都在浏览器绘制之前完成,所以 useLayoutEffect 里读取布局、再同步改样式,用户不会看到闪烁。

typescriptCode
// commit 阶段三个子阶段的调用顺序(大幅简化)
function commitRoot(root: FiberRootNode, finishedWork: Fiber) {
  // 1. before mutation:读快照、调度 passive effect
  commitBeforeMutationEffects(finishedWork);

  // 2. mutation:改 DOM、执行 useLayoutEffect 的清理、detach 旧 ref
  commitMutationEffects(root, finishedWork);

  // 关键:此刻切换双缓存树,current 指向新树
  root.current = finishedWork;

  // 3. layout:执行 useLayoutEffect 创建、componentDidMount/Update、attach 新 ref
  commitLayoutEffects(finishedWork, root);

  // useEffect 之前已被调度,浏览器绘制后异步执行
}

useEffect 与 useLayoutEffect 的执行时机对比

| 维度 | useLayoutEffect | useEffect |

| --- | --- | --- |

| 执行阶段 | commit 的 layout 子阶段 | commit 后异步(绘制之后) |

| 是否阻塞绘制 | 同步阻塞,绘制前执行 | 不阻塞,绘制后执行 |

| 典型用途 | 读布局、同步改样式防闪烁 | 数据请求、订阅、日志 |

| 执行优先级 | 同步 | NormalPriority 调度 |

| 用错的后果 | 用于重活会拖慢绘制 | 用于读布局会闪烁 |

经验法则:默认用 useEffect;只有当你需要在浏览器绘制前读取或修改 DOM 布局(否则会看到闪烁)时才用 useLayoutEffect。

Lane 模型的位运算与优先级合并

前文介绍了 Lane 的基本形态,这里深入它的位运算细节——这些运算是 React 调度的算术基础,每次更新都在跑。

为什么用 31 位而不是 32 位

Lane 是一个 32 位整数,但 React 只用其中 31 位。因为 JavaScript 的位运算按 32 位有符号整数处理,最高位(第 32 位)是符号位,用它会让数值变负、破坏 lanes & -lanes 这类技巧,所以 React 刻意留出符号位,实际车道有 31 条。

核心位运算技巧

typescriptCode
// 1. 合并优先级:一批更新可能横跨多条车道,用按位或叠加
const combined = SyncLane | DefaultLane | TransitionLane;

// 2. 取最高优先级:分离出最低位的 1(数值越小优先级越高)
// 原理:负数在补码下是"取反加一",x & -x 恰好只保留最低位的 1
function getHighestPriorityLane(lanes: number): number {
  return lanes & -lanes;
}
// 例:lanes = 0b0110 -> -lanes = 0b...1010 -> lanes & -lanes = 0b0010

// 3. 判断交集:某批 renderLanes 里是否包含目标车道
function includesSomeLane(set: number, subset: number): boolean {
  return (set & subset) !== 0;
}

// 4. 从集合中移除已完成的车道
function removeLanes(set: number, toRemove: number): number {
  return set & ~toRemove;
}

// 5. 统计有多少条车道(popcount,用于 entanglement 等场景)
function countLanes(lanes: number): number {
  let count = 0;
  while (lanes !== 0) {
    lanes &= lanes - 1; // 每次消掉最低位的 1
    count++;
  }
  return count;
}

优先级到 Lane 的映射

React 内部把事件优先级 EventPriority 映射到 Lane,再由 Scheduler 用它自己的 5 档优先级执行。这套映射决定了"点击"和"过渡"最终谁先渲染。

| 事件优先级 | 对应 Lane | Scheduler 优先级 | 超时时间 | 典型来源 |

| --- | --- | --- | --- | --- |

| DiscreteEventPriority | SyncLane | ImmediatePriority | -1(立即) | click、input、keydown |

| ContinuousEventPriority | InputContinuousLane | UserBlockingPriority | 250ms | mousemove、scroll、drag |

| DefaultEventPriority | DefaultLane | NormalPriority | 5000ms | 网络回调、定时器、初次渲染 |

| (过渡) | TransitionLane 1~16 | NormalPriority | 5000ms | startTransition 包裹的更新 |

| IdleEventPriority | IdleLane | IdlePriority | 永不超时 | 离屏/低优先级后台工作 |

TransitionLane 之所以有 16 条(TransitionLane1 到 TransitionLane16),是为了让不同的 transition 互不干扰、能被独立追踪和保留,React 会轮询分配这些车道。

饥饿问题与 lane 过期

低优先级任务可能被高优先级持续插队而永远得不到执行(饥饿)。React 的对策是给每条待处理 lane 记录一个过期时间(markStarvedLanesAsExpired):一旦某条 lane 等待超过其超时阈值,就把它标记为过期,强制以同步方式执行,避免被无限饿死。

Scheduler 包与 MessageChannel 时间分片

Scheduler 是一个独立发布的 npm 包(scheduler),与 React 解耦,可单独使用。它实现了带优先级的任务队列和时间分片,是并发能力的引擎。

为什么用 MessageChannel 而非 setTimeout

React 需要一种"把控制权还给浏览器、让它绘制,然后尽快拿回来继续干活"的机制。候选方案对比:

| 方案 | 问题 |

| --- | --- |

| requestIdleCallback | 触发不稳定、部分浏览器不支持、每秒回调频率低(约 20 次),空闲判断保守 |

| setTimeout(fn, 0) | 嵌套超过 5 层后被浏览器强制钳制到最小 4ms 延迟,浪费时间片 |

| requestAnimationFrame | 与绘制耦合,掉帧时回调间隔不稳,不适合纯计算调度 |

| MessageChannel | 宏任务、无 4ms 钳制、能在绘制后尽快触发回调,成为最终选择 |

Scheduler 用 MessageChannel 的 port.postMessage 触发一个宏任务作为 performWorkUntilDeadline 的载体,配合 5ms 的帧间隔(frameYieldMs = 5)实现让出与恢复。

typescriptCode
// Scheduler 用 MessageChannel 实现"让出后尽快恢复"(简化版)
const channel = new MessageChannel();
const port = channel.port2;
let scheduledCallback: ((hasTimeRemaining: boolean, now: number) => boolean) | null = null;

const frameInterval = 5; // 每个时间片 5ms
let startTime = -1;

function shouldYieldToHost(): boolean {
  const elapsed = performance.now() - startTime;
  return elapsed >= frameInterval; // 超过 5ms 就让出
}

const performWorkUntilDeadline = () => {
  if (scheduledCallback !== null) {
    startTime = performance.now();
    const hasMoreWork = scheduledCallback(true, startTime);
    if (hasMoreWork) {
      port.postMessage(null); // 还有活:再投递一个宏任务,先让浏览器绘制
    } else {
      scheduledCallback = null;
    }
  }
};

channel.port1.onmessage = performWorkUntilDeadline;

function schedulePerformWorkUntilDeadline() {
  port.postMessage(null);
}

任务队列:小顶堆

Scheduler 用一个基于数组的小顶堆(min-heap)管理任务,堆顶永远是"最该执行"的任务。排序键是 sortIndex:对已过期任务用 expirationTime,对未过期任务用 startTime。插入和取出都是 O(log n),避免每次遍历全部任务找最高优先级。此外还有一个 timerQueue 存放被延迟(delay)的任务,到点后转入 taskQueue。

typescriptCode
// Scheduler 核心循环 workLoop(简化)
function workLoop(hasTimeRemaining: boolean, initialTime: number): boolean {
  let currentTask = peek(taskQueue); // 取堆顶
  while (currentTask !== null) {
    if (currentTask.expirationTime > initialTime &&
        (!hasTimeRemaining || shouldYieldToHost())) {
      break; // 时间片用完且任务未过期 -> 让出
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      const didUserCallbackTimeout = currentTask.expirationTime <= initialTime;
      const continuation = callback(didUserCallbackTimeout);
      if (typeof continuation === 'function') {
        currentTask.callback = continuation; // 任务未完成,保留以便下次续跑
      } else {
        pop(taskQueue);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  return currentTask !== null; // 返回是否还有剩余工作
}

React 通过 Scheduler_scheduleCallback 把 performConcurrentWorkOnRoot 作为 callback 交给 Scheduler,Scheduler 负责在合适的时间片里驱动它,React 每处理完一批 Fiber 就 shouldYield 检查是否让出——两个模块通过这个回调契约协作。

useTransition / useDeferredValue / startTransition 源码级理解

前文给出了这三个 API 的用法,这里从实现角度讲清它们"改变了什么"。

startTransition 做了什么

startTransition 的实现异常简单:它在执行回调期间,把一个全局变量 ReactCurrentBatchConfig.transition 设为非空,于是回调里所有的 setState 触发的更新在 requestUpdateLane 时会被分配到 TransitionLane 而非 SyncLane。回调执行完立刻还原。它本身不是异步的,只是给这段时间内的更新"打了个低优先级的标签"。

typescriptCode
// startTransition 的本质(简化)
function startTransition(scope: () => void) {
  const prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = { }; // 打开 transition 上下文
  try {
    scope(); // 这里面的 setState 会被标记为 TransitionLane
  } finally {
    ReactCurrentBatchConfig.transition = prevTransition; // 还原
  }
}

// 因此:startTransition 的回调必须是同步的!
// 若在回调里 await,await 之后的 setState 已脱离 transition 上下文,不再是低优先级

这解释了一个高频误区:不能在 startTransition 回调里 await 后再 setState,因为上下文在同步执行结束时就被还原了。React 18.2+ 才对异步 transition 有更完善支持。

useTransition 比 startTransition 多了什么

useTransition 返回 [isPending, startTransition]。它比全局的 startTransition 多维护了一个 isPending 状态:在 transition 期间用一个 pending lane 触发一次高优先级更新把 isPending 置为 true,等 transition 完成再置回 false。所以 useTransition 能给你一个"正在过渡中"的布尔量做 loading 提示,代价是它绑定组件、有自己的 state。

useDeferredValue 的实现思路

useDeferredValue 内部对同一个值维持两份:当前值和上一次的值。当传入的 value 变化时,它先返回旧值(让当前渲染用旧值快速完成),同时用一个 transition 优先级调度一次重渲染去追上新值。若在追赶过程中 value 又变了,之前的追赶被丢弃。它相当于把"值的更新"降级为过渡,无需你手动包 startTransition。

tsxCode
// 三者选型:改变来源用 useTransition,无法改来源用 useDeferredValue
function App() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  // 场景一:你能控制触发点(点击切 tab),用 useTransition
  const selectTab = (next: string) => {
    startTransition(() => setTab(next)); // 切换重组件时不阻塞点击反馈
  };

  return (
    <>
      <TabBar onSelect={selectTab} activeOpacity={isPending ? 0.6 : 1} />
      <TabContent tab={tab} />
    </>
  );
}

// 场景二:值来自外部 props,你改不了触发点,用 useDeferredValue
function SlowList({ text }: { text: string }) {
  const deferredText = useDeferredValue(text); // text 变化被降级为过渡
  const items = useMemo(() => buildHugeList(deferredText), [deferredText]);
  return <List items={items} stale={text !== deferredText} />;
}

Suspense 与 Offscreen

Suspense 的工作机制

Suspense 的底层机制是"抛出 Promise"。当子组件在 Render 阶段读取尚未就绪的数据时,它 throw 一个 Promise。React 在 Render 过程中捕获到被抛出的 thenable,就把最近的 Suspense 边界标记为"挂起",渲染其 fallback,同时给这个 Promise 挂上 then 回调——Promise resolve 后 React 重新调度这棵子树的渲染。整个过程是声明式的,业务代码只管"读数据",加载协调交给 Suspense。

React 18 的并发特性让 Suspense 更进一步:切换内容时可以保留旧 UI(配合 startTransition),避免直接跳回 fallback 造成的闪烁;SSR 场景下支持选择性水合(selective hydration)和流式渲染,未就绪的 Suspense 区块以占位符先发送,数据好了再流式补上。

tsxCode
// 用 transition 让 Suspense 切换时保留旧内容,而非闪回 fallback
function Router() {
  const [page, setPage] = useState('/home');
  const [isPending, startTransition] = useTransition();

  function navigate(to: string) {
    // 包在 transition 里:新页面数据加载期间,旧页面继续显示(不闪 fallback)
    startTransition(() => setPage(to));
  }

  return (
    <Suspense fallback={<FullPageSpinner />}>
      <div style={{ opacity: isPending ? 0.7 : 1 }}>
        <Page path={page} />
      </div>
    </Suspense>
  );
}

Offscreen / Activity 组件

Offscreen(在新版 React 中对外叫 Activity)是并发架构解锁的能力:它能"预渲染"暂不显示的内容,或"隐藏但保留状态"已经不显示的内容。对应 Fiber 的 OffscreenComponent(tag = 22)。典型场景:Tab 切换时把非激活 Tab 用 Offscreen 隐藏而非卸载,其组件状态、DOM、滚动位置都被保留,切回来瞬间恢复,且隐藏期间的更新以低优先级在空闲时进行。这背后正是 Fiber 可中断、可按优先级排布工作的直接产物。

并发特性能力对比

| 特性 | 引入版本 | 作用 | 依赖 createRoot | 一句话场景 |

| --- | --- | --- | --- | --- |

| 自动批处理 | React 18 | 异步回调里也合并多次 setState | 是 | Promise/setTimeout 里连续 setState 只渲染一次 |

| startTransition | React 18 | 把更新标记为可打断的低优先级 | 是 | 切 Tab、大列表过滤不阻塞输入 |

| useTransition | React 18 | 同上并提供 isPending | 是 | 需要过渡 loading 提示时 |

| useDeferredValue | React 18 | 把某个值的更新降级为过渡 | 是 | 值来自外部无法包 startTransition |

| Suspense(数据) | React 18 | 声明式异步边界 + 流式 SSR | 是 | 数据加载协调、选择性水合 |

| Offscreen/Activity | React 18.2+ 实验 / 后续稳定 | 预渲染与保活隐藏内容 | 是 | Tab 保活、路由预取 |

| useId | React 18 | 服务端与客户端一致的稳定 id | 是 | 无障碍属性、SSR 水合一致 |

Fiber 调试方法

React DevTools Profiler

Profiler 面板可录制一次交互,展示每个组件的渲染耗时、渲染原因(props/state/context 变化)、以及是否发生了不必要的重渲染。火焰图里越宽的条代表耗时越长,是定位性能瓶颈的首选。开启 "Highlight updates when components render" 能在页面上高亮每次重渲染的组件,直观发现范围过大的更新。

内置 Profiler 组件量化渲染

tsxCode
import { Profiler, type ProfilerOnRenderCallback } from 'react';

const onRender: ProfilerOnRenderCallback = (
  id, phase, actualDuration, baseDuration, startTime, commitTime,
) => {
  // actualDuration:本次渲染实际耗时;baseDuration:无优化时的预估耗时
  // phase:'mount' | 'update' | 'nested-update'
  console.log(`[${id}] ${phase} 实际 ${actualDuration.toFixed(2)}ms / 基准 ${baseDuration.toFixed(2)}ms`);
};

function App() {
  return (
    <Profiler id="ProductList" onRender={onRender}>
      <ProductList />
    </Profiler>
  );
}

用 performance 打点观察时间分片

在 Performance 面板录制时,开启并发特性的应用会看到一次长渲染被切成多段 5ms 左右的短任务,中间穿插浏览器的绘制帧——这正是时间分片生效的可视化证据。而未开启并发(用 ReactDOM.render)的应用则是一根不被打断的长任务条。React 也会通过 User Timing API 在 Performance 面板打上组件级的标记,便于对照 Fiber 树定位。

常见调试断点

想在源码层面观察,可以在 beginWork、completeWork、commitRoot 打断点,配合 workInProgress 全局观察当前处理到哪个 Fiber;用 fiber.alternate 对照新旧两棵树的字段差异;打印 fiber.lanes 与 root.pendingLanes 观察本次渲染的优先级车道。

补充:常见坑与最佳实践(源码视角)

补充常见坑

用 index 做列表 key:diff 第二轮靠 key 建立映射,index 作 key 会在增删排序时让映射错乱,导致状态错位与多余的 Placement/Deletion。务必用稳定唯一 id。
在 Render 阶段读取 ref.current 期望是新 DOM:ref 的 attach 发生在 commit 的 layout 子阶段,Render 阶段读到的是旧值或 null。
useLayoutEffect 里放重计算:它在绘制前同步执行,重活会直接推迟首帧,表现为白屏或卡顿。重活放 useEffect。
误把 startTransition 当成防抖:它不延迟执行,只降优先级;真正需要减少触发次数仍要用 debounce/throttle。
在 startTransition 回调里 await 后 setState:await 之后已脱离 transition 上下文,setState 退回默认优先级,过渡失效。
过度依赖 memo/useMemo:bailout 本身有比较成本,对轻量组件套一层 memo 可能得不偿失,应以 Profiler 数据为准。

补充最佳实践

让协调可命中 bailout:保持 props 引用稳定(回调用 useCallback、对象用 useMemo 或提升到模块级),配合 React.memo,才能让 beginWork 早退跳过子树。
用稳定 key 帮助 diff:列表项 key 用数据主键,切勿用 index 或随机值。
按优先级组织更新:把"必须即时"的更新(输入回显、选中态)留在默认优先级,把"可以稍后"的更新(大列表、图表、路由内容)用 startTransition/useDeferredValue 降级。
副作用严格分层:读布局改样式用 useLayoutEffect,其余一律 useEffect,保持 Render 阶段纯净以适配可中断重入。
用数据驱动优化:先用 Profiler 找出真正的长任务与不必要重渲染,再针对性优化,避免凭感觉到处加 memo。

总结

Fiber 架构通过链表化的可中断数据结构、双缓存的无闪烁提交、以及基于 Lane 的优先级调度,把 React 从"一次跑完的同步渲染"升级为"可打断、可排序、可丢弃的并发渲染"。它不减少总工作量,而是通过时间分片与优先级重排,让用户交互始终获得最快响应。

| 概念 | 一句话解释 | 解决的问题 |

| --- | --- | --- |

| Fiber 节点 | 链表化的工作单元 | 让渲染可暂停、可恢复 |

| 双缓存 | current + workInProgress 两棵树 | 无闪烁提交、支持回滚 |

| 时间分片 | 每片约 5ms 后让出主线程 | 避免长任务阻塞交互 |

| Lane 模型 | 位运算表示的优先级车道 | 高效合并与调度多批更新 |

| Render/Commit | 可中断计算 + 同步提交 | 保证界面一致性与纯净性 |

| useTransition | 标记非紧急更新 | 输入等交互优先响应 |

| useDeferredValue | 延迟派生值更新 | 大列表渲染不卡输入 |

| Suspense | 声明式异步边界 | 优雅的 loading 协调与流式 SSR |