面试官问 React Fiber,这一篇文章就够了

看 React 源码或者技术文章的时候,经常会遇到 Fiber 这个词:

  • Fiber 到底是什么?为什么 React 要搞出这么个东西?
  • 都说 Fiber 让渲染可以"中断",具体是怎么实现的?
  • Fiber 和 Hooks、Suspense、Concurrent Mode 这些特性有什么关系?

这篇文章会把 Fiber 架构从头到尾讲清楚,包括它的设计动机、数据结构、工作流程,以及它是如何支撑 React 18/19 那些"并发特性"的。

为什么需要 Fiber?

Stack Reconciler 的困境

React 16 之前用的是 Stack Reconciler(栈调和器)。名字来源于它的工作方式------依赖 JavaScript 的调用栈来递归处理组件树。

当你调用 setState() 的时候,React 会从根节点开始,递归遍历整棵组件树,计算出哪些节点需要更新,然后一次性把变更应用到 DOM 上。

这个过程有个致命问题:同步且不可中断

假设你有一个包含 1000 个节点的列表,用户在输入框里打了个字,触发了状态更新。React 必须一口气把这 1000 个节点都 diff 完、更新完,才能把控制权还给浏览器。在这期间:

  • 用户输入没有响应(输入框卡住)
  • 动画掉帧(因为主线程被占用)
  • 整个页面感觉"卡顿"

问题的根源在于:所有更新都被当成同等优先级处理。用户的输入响应、动画渲染、后台数据更新,在 Stack Reconciler 眼里都一样------必须按顺序执行,谁也不能插队。

Fiber 的解题思路

2015 年,Facebook 开始开发 Fiber,2017 年随 React 16 正式发布。

Fiber 的核心思想是:把不可中断的递归调用,改成可中断的循环遍历

用 Andrew Clark(React 核心开发者)的话说:

"Fiber 是对调用栈的重新实现,专门为 React 组件设计。你可以把一个 fiber 想象成一个虚拟的栈帧,好处是你可以把这些栈帧保存在内存里,然后随时随地执行它们。"

这段话有点抽象,展开来说就是:

  1. 把大任务拆成小任务:每个组件的处理变成一个独立的"工作单元"(fiber)
  2. 每个小任务执行完都可以暂停:检查是否有更紧急的事情要做
  3. 高优先级任务可以插队:用户输入比后台数据更新重要
  4. 被中断的任务可以恢复:从上次暂停的地方继续

Fiber 数据结构

Fiber 不只是一个概念,它是一个具体的数据结构。每个 React 组件在内部都对应一个 fiber 节点,这些节点组成一棵树,但不是普通的树------是用链表串联的树。

FiberNode 的关键字段

直接看 React 源码中的 FiberNode 构造函数(简化版):

javascript 复制代码
function FiberNode(tag, pendingProps, key, mode) {
  // ========== 身份标识 ==========
  this.tag = tag;           // fiber 类型:FunctionComponent、ClassComponent、HostComponent 等
  this.key = key;           // 用于 diff 的唯一标识
  this.type = null;         // 组件函数/类,或者 DOM 标签名(如 'div')
  this.stateNode = null;    // 对应的真实 DOM 节点,或者类组件的实例

  // ========== 树结构指针 ==========
  this.return = null;       // 父节点
  this.child = null;        // 第一个子节点
  this.sibling = null;      // 下一个兄弟节点
  this.index = 0;           // 在兄弟节点中的位置

  // ========== 状态相关 ==========
  this.pendingProps = pendingProps;   // 新的 props(待处理)
  this.memoizedProps = null;          // 上次渲染用的 props
  this.memoizedState = null;          // 上次渲染的 state(Hooks 链表也存这里)
  this.updateQueue = null;            // 状态更新队列

  // ========== 副作用 ==========
  this.flags = NoFlags;               // 副作用标记:Placement、Update、Deletion 等
  this.subtreeFlags = NoFlags;        // 子树中的副作用标记
  this.deletions = null;              // 需要删除的子节点

  // ========== 调度相关 ==========
  this.lanes = NoLanes;               // 当前节点的优先级
  this.childLanes = NoLanes;          // 子树中的优先级

  // ========== 双缓冲 ==========
  this.alternate = null;              // 指向另一棵树中对应的 fiber
}

tag 字段:fiber 的类型标识

tag 决定了 React 如何处理这个 fiber。常见的类型包括:

tag 值 类型名称 说明
0 FunctionComponent 函数组件
1 ClassComponent 类组件
3 HostRoot 根节点(ReactDOM.createRoot() 创建的)
5 HostComponent 原生 DOM 元素,如 <div>
6 HostText 文本节点
7 Fragment <React.Fragment>
11 ForwardRef React.forwardRef() 创建的组件
14 MemoComponent React.memo() 包装的组件
15 SimpleMemoComponent 简单的 memo 组件

React 根据 tag 来决定调用什么方法。比如遇到 FunctionComponent 就执行函数拿返回值,遇到 ClassComponent 就调用 render() 方法。

链表树结构

Fiber 树用三个指针串联:

kotlin 复制代码
         ┌─────────────────────────────────────────────┐
         │                   App                        │
         │          (return: null)                      │
         └─────────────────────────────────────────────┘
                           │
                         child
                           ↓
         ┌─────────────────────────────────────────────┐
         │                 Header                       │
         │            (return: App)                     │
         └─────────────────────────────────────────────┘
                           │
                         child                  sibling
                           ↓                       ↓
         ┌─────────────────┐              ┌─────────────────┐
         │      Logo       │   sibling    │      Nav        │
         │  (return: Header)│ ─────────→  │  (return: Header)│
         └─────────────────┘              └─────────────────┘
  • child:指向第一个子节点
  • sibling:指向下一个兄弟节点
  • return:指向父节点

为什么用链表而不是数组存子节点?因为链表可以方便地暂停和恢复遍历------只需要记住当前处理到哪个节点就行。

双缓冲机制

Fiber 使用"双缓冲"技术,同时维护两棵树:

  • current 树:当前屏幕上显示的内容
  • workInProgress 树:正在构建的新树

两棵树的节点通过 alternate 指针互相引用:

scss 复制代码
   current 树                    workInProgress 树

   ┌─────────┐      alternate     ┌─────────┐
   │  App    │ ←─────────────────→│  App    │
   │(current)│                    │ (WIP)   │
   └─────────┘                    └─────────┘
       │                              │
   ┌─────────┐      alternate     ┌─────────┐
   │ Header  │ ←─────────────────→│ Header  │
   │(current)│                    │ (WIP)   │
   └─────────┘                    └─────────┘

这个设计的好处:

  1. 渲染过程中不影响当前显示:所有变更都在 workInProgress 树上进行
  2. 原子化提交:构建完成后,直接把 workInProgress 变成 current(交换指针)
  3. 复用 fiber 节点:下次更新时,current 树变成 workInProgress 树的基础,减少内存分配

工作循环:Fiber 如何执行

Fiber 的核心执行逻辑在 workLoop 函数中。整个过程分为两个阶段:Render 阶段Commit 阶段

整体流程

flowchart TB subgraph render["Render 阶段(可中断)"] direction TB A[开始更新] --> B[选取下一个工作单元] B --> C{有工作单元?} C -->|是| D[beginWork: 处理当前节点] D --> E{有子节点?} E -->|是| B E -->|否| F[completeWork: 完成当前节点] F --> G{有兄弟节点?} G -->|是| B G -->|否| H[返回父节点继续 complete] H --> I{回到根节点?} I -->|否| G I -->|是| J[Render 阶段结束] C -->|否| J end subgraph commit["Commit 阶段(不可中断)"] direction TB K[开始提交] --> L[Before Mutation] L --> M[Mutation: 操作 DOM] M --> N[Layout: 执行副作用] N --> O[完成] end J --> K style render fill:#cce5ff,stroke:#0d6efd style commit fill:#d4edda,stroke:#28a745

Render 阶段:构建 workInProgress 树

Render 阶段的目标是:遍历 fiber 树,找出哪些节点需要更新,打上标记(flags),构建完整的 workInProgress 树。

这个阶段是可中断的------React 可以在处理完任意一个 fiber 后暂停,把控制权交还给浏览器。

workLoop:工作循环

javascript 复制代码
function workLoop() {
  // 循环处理每个工作单元
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;

  // 1. beginWork:处理当前节点,返回子节点
  const next = beginWork(current, unitOfWork, renderLanes);

  // 2. 更新 memoizedProps
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    // 没有子节点了,完成当前节点的工作
    completeUnitOfWork(unitOfWork);
  } else {
    // 有子节点,继续处理子节点
    workInProgress = next;
  }
}

beginWork:向下遍历,标记更新

beginWork 负责处理当前 fiber 节点,主要做这些事:

  1. 判断是否可以跳过:如果 props 和 state 都没变,直接跳过这个子树
  2. 根据 fiber 类型执行不同逻辑:函数组件就执行函数,类组件就调用 render 方法
  3. 创建/更新子 fiber 节点:对比新旧 children,进行 diff
  4. 返回第一个子 fiber:作为下一个工作单元
javascript 复制代码
function beginWork(current, workInProgress, renderLanes) {
  // 优化:如果没有更新,可以跳过
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (oldProps === newProps && !hasContextChanged()) {
      // 没有变化,尝试跳过
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  }

  // 根据 fiber 类型分发处理
  switch (workInProgress.tag) {
    case FunctionComponent:
      return updateFunctionComponent(current, workInProgress, renderLanes);
    case ClassComponent:
      return updateClassComponent(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    // ... 其他类型
  }
}

completeWork:向上回溯,准备 DOM

当一个 fiber 节点没有子节点(或所有子节点都处理完了),就会调用 completeWork

  1. 创建/更新真实 DOM 节点:但还不挂载到页面上
  2. 收集副作用:把有 flags 的节点串成链表,方便 Commit 阶段处理
  3. 冒泡 subtreeFlags:让父节点知道子树中有没有需要处理的副作用
javascript 复制代码
function completeWork(current, workInProgress, renderLanes) {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case HostComponent: {
      const type = workInProgress.type; // 如 'div'

      if (current !== null && workInProgress.stateNode !== null) {
        // 更新:对比新旧 props,计算需要更新的属性
        updateHostComponent(current, workInProgress, type, newProps);
      } else {
        // 新建:创建 DOM 元素
        const instance = createInstance(type, newProps);
        // 把所有子 DOM 节点挂到这个元素上
        appendAllChildren(instance, workInProgress);
        workInProgress.stateNode = instance;
      }

      // 收集副作用标记到父节点
      bubbleProperties(workInProgress);
      return null;
    }
    // ... 其他类型
  }
}

遍历顺序示意

假设有这样一棵组件树:

css 复制代码
       App
      /   \
   Header  Main
    /  \      \
  Logo  Nav   Content

Fiber 的遍历顺序是深度优先,但会在每个节点上执行 beginWork(向下)和 completeWork(向上):

scss 复制代码
1. beginWork(App)
2. beginWork(Header)
3. beginWork(Logo)
4. completeWork(Logo)     ← Logo 没有子节点,开始 complete
5. beginWork(Nav)         ← 回到 Header,处理下一个子节点
6. completeWork(Nav)
7. completeWork(Header)   ← Header 所有子节点处理完,complete 自己
8. beginWork(Main)
9. beginWork(Content)
10. completeWork(Content)
11. completeWork(Main)
12. completeWork(App)     ← 回到根节点,Render 阶段结束

Commit 阶段:应用变更

Render 阶段完成后,workInProgress 树已经构建好了,所有需要的变更也都标记好了。接下来就是 Commit 阶段------把这些变更实际应用到 DOM 上。

Commit 阶段是同步的、不可中断的。因为这个阶段要操作真实 DOM,必须一次性完成,否则用户会看到不一致的 UI。

Commit 阶段分为三个子阶段:

1. Before Mutation(DOM 操作前)

  • 调用 getSnapshotBeforeUpdate 生命周期方法
  • 调度 useEffect 的清理函数(异步)

2. Mutation(执行 DOM 操作)

这是真正修改 DOM 的阶段:

javascript 复制代码
function commitMutationEffects(root, finishedWork) {
  while (nextEffect !== null) {
    const flags = nextEffect.flags;

    // 处理 DOM 插入
    if (flags & Placement) {
      commitPlacement(nextEffect);
    }

    // 处理 DOM 更新
    if (flags & Update) {
      commitWork(current, nextEffect);
    }

    // 处理 DOM 删除
    if (flags & Deletion) {
      commitDeletion(root, nextEffect);
    }

    nextEffect = nextEffect.nextEffect;
  }
}

3. Layout(DOM 操作后)

  • 调用 componentDidMount / componentDidUpdate
  • 调用 useLayoutEffect 的回调
  • 更新 ref

最后,React 把 current 指针指向 workInProgress 树,完成树的切换。

优先级调度:让重要的事先做

Fiber 架构的一大优势是支持优先级调度。不是所有更新都同等重要------用户输入应该比后台数据刷新更快响应。

Lane 优先级模型

React 18 使用 Lane 模型来表示优先级。每个优先级是一个 32 位整数中的一个位:

javascript 复制代码
const NoLanes = 0b0000000000000000000000000000000;
const SyncLane = 0b0000000000000000000000000000001;         // 最高优先级:同步
const InputContinuousLane = 0b0000000000000000000000000100; // 连续输入,如拖拽
const DefaultLane = 0b0000000000000000000000000010000;      // 默认优先级
const TransitionLane1 = 0b0000000000000000000001000000;     // Transition
const IdleLane = 0b0100000000000000000000000000000;         // 空闲时执行

位运算的好处是可以高效地合并、比较多个优先级:

javascript 复制代码
// 合并优先级
const mergedLanes = lane1 | lane2;

// 判断是否包含某优先级
const includesLane = (lanes & lane) !== 0;

// 获取最高优先级
const highestLane = lanes & -lanes;

Scheduler:任务调度器

React 有一个独立的 Scheduler 包,负责按优先级调度任务。它的核心思想是时间切片(Time Slicing):

  1. 把渲染工作拆成多个小任务
  2. 每个小任务执行完后,检查是否有更高优先级的任务
  3. 如果有,暂停当前工作,先处理高优先级任务
  4. 如果时间片用完了(通常是 5ms),让出主线程给浏览器
javascript 复制代码
function workLoopConcurrent() {
  // 循环执行工作单元,但会在时间片用完时暂停
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

function shouldYield() {
  // 检查是否还有剩余时间,或者是否有更高优先级的任务
  return getCurrentTime() >= deadline || hasHigherPriorityWork();
}

时间切片的效果

没有时间切片的情况下,一个耗时 200ms 的渲染任务会完全阻塞主线程:

css 复制代码
主线程:[==== 200ms 渲染任务 ====]
         ^                    ^
         |                    |
    用户点击                200ms 后才响应

有时间切片后,渲染任务被拆成多个 5ms 的小块,中间可以响应用户交互:

css 复制代码
主线程:[5ms][响应点击][5ms][5ms][5ms]...[5ms]
         ^      ^
         |      |
    用户点击   立即响应

startTransition:标记低优先级更新

React 18 提供了 startTransition API,让开发者可以手动标记哪些更新是"可以延迟的":

javascript 复制代码
import { startTransition } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  function handleChange(e) {
    // 高优先级:立即更新输入框
    setQuery(e.target.value);

    // 低优先级:搜索结果可以稍后更新
    startTransition(() => {
      setResults(search(e.target.value));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      <SearchResults results={results} />
    </>
  );
}

在这个例子中,用户每次输入都会触发两个更新:

  1. setQuery:高优先级,输入框立即响应
  2. setResults:低优先级,包裹在 startTransition 里,可以被打断

如果用户快速输入,搜索结果的渲染可能会被跳过几次,直到用户停止输入。这保证了输入框始终流畅响应,而不会被搜索结果的渲染阻塞。

Hooks 与 Fiber 的关系

Hooks 是怎么实现的?答案就在 Fiber 节点的 memoizedState 字段里。

Hooks 链表

每个函数组件的 fiber 节点都有一个 memoizedState 字段,存储着该组件所有 hooks 的状态。这些状态以链表的形式串联:

scss 复制代码
FiberNode
├── memoizedState ─→ Hook1 ─→ Hook2 ─→ Hook3 ─→ null
│                    (useState) (useEffect) (useMemo)
└── ...

每个 hook 节点的结构大致如下:

javascript 复制代码
const hook = {
  memoizedState: any,     // 这个 hook 存储的值
  baseState: any,         // 更新前的基础状态
  baseQueue: Update | null,
  queue: UpdateQueue,     // 待处理的更新队列
  next: Hook | null       // 指向下一个 hook
};

useState 的实现原理

当你在组件里调用 useState(0) 时,React 做的事情:

首次渲染(mount):

javascript 复制代码
function mountState(initialState) {
  // 1. 创建一个新的 hook 节点,挂到链表上
  const hook = mountWorkInProgressHook();

  // 2. 计算初始值
  if (typeof initialState === 'function') {
    initialState = initialState();
  }

  // 3. 保存状态
  hook.memoizedState = hook.baseState = initialState;

  // 4. 创建更新队列
  hook.queue = {
    pending: null,
    dispatch: null,
    // ...
  };

  // 5. 返回 [state, setState]
  const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, hook.queue);
  hook.queue.dispatch = dispatch;

  return [hook.memoizedState, dispatch];
}

后续更新(update):

javascript 复制代码
function updateState() {
  // 1. 找到当前 hook(按顺序从链表取)
  const hook = updateWorkInProgressHook();

  // 2. 处理所有待处理的更新
  const queue = hook.queue;
  let newState = hook.baseState;

  let update = queue.pending;
  while (update !== null) {
    // 应用每个更新
    newState = typeof update.action === 'function'
      ? update.action(newState)
      : update.action;
    update = update.next;
  }

  // 3. 保存新状态
  hook.memoizedState = newState;

  return [newState, queue.dispatch];
}

为什么 Hooks 不能在条件语句里调用?

因为 hooks 是按调用顺序存储在链表里的,React 靠顺序来匹配每次渲染时的 hook。

假设你这样写:

javascript 复制代码
function Bad() {
  const [count, setCount] = useState(0);

  if (count > 0) {
    useEffect(() => { /* ... */ }); // 危险!
  }

  const [name, setName] = useState('');
  // ...
}

第一次渲染时 count 是 0,hooks 链表是:

scss 复制代码
Hook1(useState: count) → Hook2(useState: name) → null

第二次渲染时 count 变成 1,useEffect 被执行了,链表变成:

scss 复制代码
Hook1(useState: count) → Hook2(useEffect) → Hook3(useState: name) → null

React 按顺序取 hook,第二个位置取到了 useEffect,但代码里第二个 hook 是 useState,类型对不上,直接报错。

所以 React 的规则是:Hooks 只能在函数组件的最顶层调用,不能在条件、循环或嵌套函数里。

useEffect 的实现原理

useEffect 的状态存储方式略有不同,它的副作用会被收集到 fiber 的 updateQueue 中:

javascript 复制代码
function mountEffect(create, deps) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;

  // 标记这个 fiber 有 Passive 副作用
  currentlyRenderingFiber.flags |= PassiveEffect;

  // 创建 effect 对象,挂到 fiber 的 updateQueue
  hook.memoizedState = pushEffect(
    HasEffect | Passive,
    create,
    undefined,
    nextDeps
  );
}

Effect 链表结构:

javascript 复制代码
FiberNode.updateQueue.effects
    ↓
  Effect1 → Effect2 → Effect3 → Effect1 (循环链表)
  ├── create: () => { ... }     // 执行的函数
  ├── destroy: () => { ... }    // 清理函数(上次 create 的返回值)
  ├── deps: [a, b]              // 依赖数组
  └── next: Effect2

在 Commit 阶段完成后,React 会异步执行所有 Passive 类型的 effects:

  1. 先执行所有 effect 的 destroy(清理函数)
  2. 再执行所有 effect 的 create(新的副作用)

这也是为什么 useEffect 总是在 DOM 更新后异步执行。

Fiber 支撑的高级特性

Fiber 架构不只是让渲染可以中断,它还是很多 React 高级特性的基础。

Suspense:优雅处理异步

Suspense 让你可以在等待异步数据时显示 fallback UI:

javascript 复制代码
function ProfilePage() {
  return (
    <Suspense fallback={<Spinner />}>
      <ProfileDetails />
    </Suspense>
  );
}

function ProfileDetails() {
  const user = use(fetchUser()); // 这个 promise 还没 resolve 时会"挂起"
  return <h1>{user.name}</h1>;
}

Suspense 的实现依赖 Fiber 的以下能力:

  1. 抛出 Promise:当组件需要等待异步数据时,可以 throw 一个 Promise
  2. 捕获并暂停:Fiber 在遍历时捕获这个 Promise,标记当前子树为"挂起"状态
  3. 显示 fallback:渲染最近的 Suspense 边界的 fallback
  4. 恢复渲染:Promise resolve 后,从挂起的地方重新开始渲染
sequenceDiagram participant App participant Suspense participant ProfileDetails participant Fiber App->>Suspense: 渲染 Suspense->>ProfileDetails: 渲染 ProfileDetails->>Fiber: throw Promise Fiber->>Suspense: 捕获 Promise,显示 fallback Note over Suspense: 显示 ProfileDetails-->>Fiber: Promise resolved Fiber->>ProfileDetails: 重新渲染 ProfileDetails->>Suspense: 返回真实内容 Note over Suspense: 显示

用户名

Concurrent Rendering:并发渲染

React 18 的并发模式完全建立在 Fiber 之上。它的核心能力是:React 可以同时准备多个版本的 UI

比如使用 useTransition

javascript 复制代码
function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('home');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <>
      <TabButton onClick={() => selectTab('home')}>Home</TabButton>
      <TabButton onClick={() => selectTab('posts')}>Posts</TabButton>
      <TabButton onClick={() => selectTab('contact')}>Contact</TabButton>

      {isPending && <Spinner />}

      <TabPanel tab={tab} />
    </>
  );
}

当用户点击 "Posts" tab 时:

  1. React 开始在"后台"渲染 Posts 页面(构建 workInProgress 树)
  2. 同时保持显示当前的 Home 页面(current 树不变)
  3. 如果 Posts 渲染很慢,用户可以点击其他 tab,之前的渲染会被放弃
  4. 渲染完成后,React 才会切换到新的 UI

这就是"并发"的含义:多个渲染任务可以同时存在,React 可以在它们之间切换。

Error Boundaries:错误边界

Error Boundaries 也依赖 Fiber 的树结构:

javascript 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logError(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

当子组件抛出错误时,Fiber 会:

  1. 沿着 return 指针向上查找最近的 Error Boundary
  2. 标记该 Error Boundary 的 fiber 有 ShouldCapture flag
  3. 在 Commit 阶段调用 componentDidCatch
  4. 重新渲染 Error Boundary,显示降级 UI

Server Components:服务端组件

React Server Components 也依赖 Fiber 架构。服务端渲染时,React 会:

  1. 在服务端构建 Fiber 树
  2. 将 Fiber 树序列化成特殊的"Flight"格式
  3. 客户端接收后,重建 Fiber 树,进行 Hydration

由于 Fiber 是一个可序列化的数据结构(本质上是一棵树),它可以在服务端和客户端之间传输。

实际影响:性能优化建议

理解了 Fiber 架构,可以更好地进行性能优化。

1. 避免频繁创建新对象

每次渲染时创建新对象会导致 props 变化,触发不必要的子组件更新:

javascript 复制代码
// 差:每次渲染都创建新的 style 对象
function Bad() {
  return <div style={{ color: 'red' }}>...</div>;
}

// 好:把常量提到组件外面
const style = { color: 'red' };
function Good() {
  return <div style={style}>...</div>;
}

在 Fiber 的 beginWork 阶段,React 会比较新旧 props。如果是同一个引用,可以快速跳过子树的处理。

2. 合理使用 key

key 帮助 React 在 diff 时识别哪些元素改变了位置:

javascript 复制代码
// 差:用 index 作为 key,列表重排序时会有问题
items.map((item, index) => <Item key={index} {...item} />)

// 好:用稳定的唯一 ID
items.map(item => <Item key={item.id} {...item} />)

Fiber 在 reconcile children 时,会用 key 来复用已有的 fiber 节点,而不是删除再创建。

3. 使用 startTransition 处理大量更新

如果某个操作会触发大量组件更新(比如筛选长列表),用 startTransition 标记为低优先级:

javascript 复制代码
function FilteredList({ items }) {
  const [filter, setFilter] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);

  function handleFilterChange(e) {
    const value = e.target.value;
    setFilter(value); // 高优先级:输入框立即响应

    startTransition(() => {
      // 低优先级:过滤操作可以稍后执行
      setFilteredItems(items.filter(item =>
        item.name.includes(value)
      ));
    });
  }

  return (
    <>
      <input value={filter} onChange={handleFilterChange} />
      <ItemList items={filteredItems} />
    </>
  );
}

4. 使用 useDeferredValue 延迟非关键更新

useDeferredValue 可以让某个值的更新延后:

javascript 复制代码
function SearchResults({ query }) {
  // 这个值会延迟更新,不阻塞输入
  const deferredQuery = useDeferredValue(query);

  // 基于延迟的值进行渲染
  const results = useMemo(
    () => slowSearch(deferredQuery),
    [deferredQuery]
  );

  return <ResultList results={results} />;
}

5. Suspense 配合 lazy 做代码分割

javascript 复制代码
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

Fiber 会在 HeavyComponent 加载时挂起,显示 Loading,加载完成后自动恢复渲染。

调试 Fiber

React DevTools 可以直接查看 Fiber 树。打开 DevTools 的 Components 面板,你看到的组件树本质上就是 Fiber 树的可视化。

你还可以在浏览器控制台里访问 fiber:

javascript 复制代码
// 获取某个 DOM 元素对应的 fiber
const fiber = domElement._reactFiber$...; // 键名是动态的

// 查看 fiber 的结构
console.log(fiber.type);         // 组件类型
console.log(fiber.memoizedState); // 状态(hooks 链表)
console.log(fiber.memoizedProps); // props
console.log(fiber.child);        // 第一个子节点
console.log(fiber.sibling);      // 兄弟节点
console.log(fiber.return);       // 父节点

总结

React Fiber 是 React 从 16 版本开始的核心架构,它解决了旧版 Stack Reconciler 的几个关键问题:

  1. 可中断渲染:把递归调用改成循环遍历,每处理完一个 fiber 就可以暂停
  2. 优先级调度:不同类型的更新有不同优先级,重要的先做
  3. 并发渲染:可以同时准备多个版本的 UI,按需切换
  4. 增量渲染:把渲染工作拆成小块,分散到多个帧中

Fiber 的核心是一个链表树结构的数据模型,加上双缓冲技术和两阶段渲染(Render + Commit)。它是 Hooks、Suspense、Concurrent Mode、Server Components 等现代 React 特性的基础设施。

理解 Fiber 不是为了在日常开发中直接操作它,而是为了:

  • 理解 React 的工作原理,写出更高效的代码
  • 更好地使用 startTransitionuseDeferredValue 等 API
  • 排查性能问题时知道从哪里入手
  • 读懂 React 源码和技术文章

最后放几个深入学习的资源:


如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:

Claude Code Skills (按需加载,意图自动识别,不浪费 token,介绍文章):

全栈项目(适合学习现代技术栈):

  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB
相关推荐
LYFlied5 小时前
【一句话概述】Webpack、Vite、Rollup 核心区别
前端·webpack·node.js·rollup·vite·打包·一句话概述
reddingtons5 小时前
PS 参考图像:线稿上色太慢?AI 3秒“喂”出精细厚涂
前端·人工智能·游戏·ui·aigc·游戏策划·游戏美术
一水鉴天5 小时前
整体设计 定稿 之23+ dashboard.html 增加三层次动态记录体系仪表盘 之2 程序 (Q199 之2) (codebuddy)
开发语言·前端·javascript
刘发财5 小时前
前端一行代码生成数千页PDF,dompdf.js新增分页功能
前端·typescript·开源
_请输入用户名5 小时前
Vue 3 源码项目结构详解
前端·vue.js
少卿5 小时前
Next.js 国际化实现方案详解
前端·next.js
掘金挖土5 小时前
手摸手快速搭建 Vue3 + ElementPlus 后台管理系统模板,使用 JavaScript
前端·javascript
CoderHing5 小时前
告别 try/catch 地狱:用三元组重新定义 JavaScript 错误处理
前端·javascript·react.js
一念之间lq5 小时前
Elpis 第三阶段· 领域模型架构建设
前端·后端