Fiber 架构:React 的革命性升级
为什么需要 Fiber?
React 15 的问题:
javascript
// 假设有 10000 个组件需要更新
function App() {
return (
<div>
{Array.from({ length: 10000 }, (_, i) => <Item key={i} />)}
</div>
);
}
// React 15:同步渲染,一旦开始就不能停
// 主线程被占用 → 用户点击按钮没反应 → 页面卡顿
Fiber 的解决方案:可中断的渲染
javascript
传统:渲染 10000 个组件(阻塞 500ms)→ 用户操作无响应
Fiber:渲染 100 个 → 检查是否有高优先级任务 → 有则暂停 → 处理用户点击 → 继续渲染
Fiber 是什么?
Fiber = 虚拟栈帧 = 工作单元
javascript
// 简化的 Fiber 节点结构
const fiber = {
type: 'div', // 组件类型
props: { className: 'box' }, // 属性
stateNode: domElement, // 真实 DOM 节点
// 链表结构(用于遍历)
child: childFiber, // 第一个子节点
sibling: siblingFiber, // 下一个兄弟节点
return: parentFiber, // 父节点
// 副作用
effectTag: 'UPDATE', // 'PLACEMENT' | 'UPDATE' | 'DELETION'
alternate: oldFiber, // 上一次的 Fiber(用于 Diff)
};
Fiber 树的遍历:
javascript
// JSX
<App>
<Header>
<Title />
</Header>
<Content />
</App>
// Fiber 树(链表结构)
App├─ child → Header
│├─ child → Title
│ └─ return → Header
└─ sibling → Content
// 遍历顺序(深度优先)
App → Header → Title → Content
时间切片(Time Slicing)
javascript
// React 内部的调度逻辑(简化版)
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 处理一个 Fiber
shouldYield = deadline.timeRemaining() < 1; // 剩余时间不足 1ms
}
if (nextUnitOfWork) {
// 还有工作未完成,下一帧继续
requestIdleCallback(workLoop);
} else {
// 完成,提交更新到 DOM
commitRoot();
}
}
requestIdleCallback(workLoop);
代码解释:
javascript
// React 内部的调度逻辑(简化版)
function workLoop(deadline) {
// 1. 标记:是否需要【让出主线程】给浏览器
let shouldYield = false;
// 2. 核心循环:只要有任务 + 不需要让出,就一直执行
// nextUnitOfWork = 下一个要处理的 Fiber 节点(最小工作单元)
while (nextUnitOfWork && !shouldYield) {
// 3. 处理【单个 Fiber 任务】(diff、标记更新、渲染组件)
// 执行完一个,返回下一个待处理的 Fiber
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// 4. 检查浏览器【剩余空闲时间】
// 小于 1ms → 时间不够了,必须暂停!
shouldYield = deadline.timeRemaining() < 1;
}
// 5. 如果还有未完成的任务
if (nextUnitOfWork) {
// 【主动暂停】,等浏览器下一帧空闲时,继续执行
requestIdleCallback(workLoop);
} else {
// 6. 所有任务全部完成!一次性更新到真实 DOM
commitRoot();
}
}
// 启动工作循环(浏览器空闲时执行)
requestIdleCallback(workLoop);
关键变量 / 函数解释
workLoopReact 的工作循环,所有渲染任务都在这里排队执行。deadline浏览器requestIdleCallback自动传入的对象,告诉你当前帧还剩多少空闲时间。- **
nextUnitOfWork**最小工作单元 = 一个 Fiber 节点(对应一个组件 / 一个 DOM)。- **
performUnitOfWork**干实事的函数:处理单个 Fiber(执行组件、diff 对比、标记更新)。requestIdleCallback浏览器 API:在帧空闲时执行回调,不阻塞渲染 / 交互。commitRoot()最终把内存里的更新,一次性同步渲染到真实 DOM。
实际效果:
javascript
帧 1 (16ms):渲染 100 个组件 (5ms) → 空闲 (11ms) → 处理用户输入
帧 2 (16ms):渲染 100 个组件 (5ms) → 空闲 (11ms) → 动画流畅
帧 3 (16ms):渲染 100 个组件 (5ms) → ...
补充:React 真实实现的小细节
React 源码没有直接用
requestIdleCallback,而是用MessageChannel替代:
- 原因:
requestIdleCallback兼容性差、触发不稳定(最低 20ms 间隔)- 原理:
MessageChannel可以实现微任务级精确调度,切分 5ms 任务片- 结果:效果和这段伪代码完全一致,只是精度更高。
调和过程(Reconciliation):Render 和 Commit
两阶段架构:
javascript
Render 阶段(可中断) Commit 阶段(不可中断)
├─ 构建 Fiber 树 ├─ 操作真实 DOM
├─ Diff 算法 ├─ 执行副作用
├─ 标记副作用 └─ 调用生命周期
└─ 可以被高优先级任务打断
Render 阶段详解
- React 创建更新,计算优先级(Lanes)
- 进入 workLoop 时间切片
- 在浏览器帧空闲时执行
- 每次执行一小段 Fiber,检查时间
- 时间不够 → 让出主线程 → 下帧继续
- 直到:整棵 Fiber 树 diff 完成 → 生成副作用列表→ Render 阶段结束
Render 阶段可能跨 1 帧~N 帧,但不阻塞页面。
javascript
function reconcileChildren(fiber, children) {
let oldFiber = fiber.alternate?.child; // 旧 Fiber
let prevSibling = null;
children.forEach((child, index) => {
const newFiber = {
type: child.type,
props: child.props,
return: fiber,};
// Diff 算法
if (oldFiber && oldFiber.type === child.type) {
// 类型相同 → 更新
newFiber.alternate = oldFiber;
newFiber.effectTag = 'UPDATE';
newFiber.stateNode = oldFiber.stateNode;} else {
// 类型不同 → 替换
newFiber.effectTag = 'PLACEMENT';
}
// 构建链表
if (index === 0) {
fiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
oldFiber = oldFiber?.sibling;
});
}
Commit 阶段详解
- Before Mutation(DOM变更前): 执行类组件
getSnapshotBeforeUpdate - Mutation 阶段:真实 DOM 更新→ DOM 变了,但浏览器还没渲染
- Layout 阶段:同步执行 useLayoutEffect → 你可以在这里读取最新布局、强制修改样式、避免闪烁, 执行类组件
componentDidMount / DidUpdate - 同步执行 useLayoutEffect 创建函数
- 此时:DOM 已变,但屏幕还没显示新画面
javascript
function commitRoot() {
// 阶段 1:before mutation(DOM 变更前)
commitBeforeMutationEffects(); // 调用 getSnapshotBeforeUpdate
// 阶段 2:mutation(DOM 变更)
commitMutationEffects(); // 插入、更新、删除 DOM
// 阶段 3:layout(DOM 变更后)
commitLayoutEffects(); // 调用 componentDidMount/Update、useLayoutEffect
}
function commitMutationEffects(fiber) {
if (fiber.effectTag === 'PLACEMENT') {
// 插入 DOM
fiber.return.stateNode.appendChild(fiber.stateNode);
} else if (fiber.effectTag === 'UPDATE') {
// 更新属性
updateDOMProperties(fiber.stateNode, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === 'DELETION') {
// 删除 DOM
fiber.return.stateNode.removeChild(fiber.stateNode);
}
}
实战案例:为什么 useLayoutEffect 比 useEffect 先执行?
javascript
function Component() {
useEffect(() => {
console.log('3. useEffect'); // 异步执行(宏任务)
});
useLayoutEffect(() => {
console.log('2. useLayoutEffect'); // 同步执行(Commit 阶段)
});
console.log('1. render');
return <div>Hello</div>;
}
// 输出顺序:
// 1. render
// 2. useLayoutEffect(DOM 更新后立即执行,阻塞浏览器绘制)
// 3. useEffect(浏览器绘制后执行)
使用场景:
javascript
// ✅ useLayoutEffect:需要同步读取 DOM 布局
function Tooltip() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const ref = useRef(null);
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect();
setPosition({ x: rect.left, y: rect.top }); // 避免闪烁
}, []);
return <div ref={ref} style={{ left: position.x, top: position.y }}>提示</div>;
}
// ✅ useEffect:异步操作(请求、订阅)
function DataFetcher() {
useEffect(() => {
fetch('/api/data').then(setData); // 不阻塞渲染
}, []);
}
React Fiber + Concurrent 完整流程:

React Render+Commit+浏览器原生帧流程梳理:
浏览器原生:一帧完整生命周期:
浏览器一帧 ≈ 16ms(60fps),永远按下面顺序执行:
第 1 步:执行 JS(宏任务 → 清空微任务):
从宏任务队列取一个任务执行(如:click、setTimeout、script、requestIdleCallback)
执行过程中产生的所有微任务 (Promise.then、queueMicrotask、React 内部调度)→ 必须在本帧同步清空
直到 JS 调用栈空 + 微任务空 → 才会进入渲染流程
第 2 步:浏览器渲染流水线(不可中断):
Style Recalculate(样式重计算)
Layout / Reflow(布局:几何位置、宽高)
Paint(绘制:颜色、图片、文字)
Composite(合成:图层合并)
第 3 步:进入空闲 / 等待下一帧:
有
requestIdleCallback→ 此时执行时间到 → 进入下一帧,重复上述流程
串联:
用户触发更新:
用户触发更新(setState /props change / 父组件重渲染)→ React Render 阶段 (diff Fiber、可中断、时间切片)→ React Commit 阶段 • 真实 DOM 更新 • 同步执行 useLayoutEffect → 回到浏览器原生渲染流水线(浏览器渲染):浏览器 重排 (Layout) + 重绘 (Paint) (屏幕才真正变)→ 异步执行 useEffect :渲染完全结束,React 将 useEffect 放入宏任务 ,在下一次空闲时执行→ 不阻塞渲染,不影响页面流畅度。
网页第一次渲染(挂载阶段):
同步执行入口 JS(createRoot /root.render)→ React Render 阶段 (首次构建 Fiber 树,无 diff)→ React Commit 阶段 • DOM 插入(第一次挂到页面) • 同步执行 useLayoutEffect → 浏览器 Layout + Paint (首次显示页面)→ 异步执行 useEffect。
不管是首次渲染 还是更新:
- 浏览器执行 JS、微任务
- Render 阶段:可中断、不操作 DOM、时间切片、跨帧
- Commit 阶段:同步不可中断、更 DOM、执行 useLayoutEffect
- 先 Commit,再浏览器绘制: Layout + Paint(屏幕显示)
- useEffect 永远在绘制完才异步执行
易混淆:
只有 React Commit 阶段是:同步、阻塞、占用当前帧;React 调和阶段(Render 阶段):异步、可中断、非阻塞、和浏览器帧交替执行。
浏览器一帧(16ms):JS 执行、样式计算、Layout、Paint、Composite。
Fiber架构下真实流程:Render 阶段 :跑在帧与帧之间的空闲时间 执行 5ms → 暂停 → 浏览器渲染 → 再执行 5ms→ 完全不阻塞帧流程;Commit 阶段 :插到某一帧的 JS 执行期 同步跑完 → 然后浏览器走 Layout/Paint→ 短暂阻塞,但极快。
易混淆:DOM 更新不是浏览器绘制:
- DOM 更新 :JS 操作 DOM 树,发生在 Commit 阶段
- 浏览器绘制 :把 DOM 画到屏幕,发生在 Commit 之后
Q:useLayoutEffect 为什么能 "防闪烁"?
A:因为它在:DOM 已更新 → 浏览器绘制前 同步执行,你可以在这里改样式,浏览器只画最终结果。
Q:useEffect 为什么不卡?
A:因为它在:浏览器绘制完成后 才异步执行不参与帧内同步工作,绝不阻塞渲染。
React18 Q&A:
1. React18 最核心的更新是什么?
答 :默认开启并发渲染(Concurrent Rendering) ,提供可中断、可优先级、可插队 的更新机制,同时带来自动批处理、useTransition、Suspense 增强、流式 SSR 等核心能力。
2. React18 如何开启并发特性?
答:用新的根节点 API 替换旧 API:
javascript
// 旧
ReactDOM.render(<App />, el)
// React18
const root = ReactDOM.createRoot(el)
root.render(<App />)
只有用 createRoot,并发、自动批处理、新 Hooks 才会生效。
3. 什么是自动批处理(Automatic Batching)?
答:
- 批处理:把多次
setState合并成一次渲染,提升性能。 - React18 之前:只在合成事件、生命周期里批处理,Promise、setTimeout、原生事件里不批。
- React18:所有场景自动批处理,不管是异步、Promise、定时器、原生事件,都会合并更新。
4. React18 移除 / 废弃了哪些 API?
答:
- 淘汰
ReactDOM.render,改用createRoot - 淘汰
unmountComponentAtNode,改用root.unmount() - 淘汰
ReactDOM.hydrate,改用hydrateRoot - 废弃
useEffect依赖为undefined等旧写法
并发核心:
5. 什么是 Concurrent Mode(并发模式)?
答 :不是功能,是底层渲染机制:
- 渲染过程可中断、可恢复、可插队、可废弃
- 高优先级任务(输入、点击)可以打断低优先级任务(列表渲染)
- 保证主线程不阻塞,页面永远流畅
6. useTransition 是什么?有什么用?
答:
- 把更新标记为非紧急、可中断的过渡更新
- 不会阻塞用户交互、输入
- 返回
[isPending, startTransition] - 适用:大数据列表、搜索过滤、图表渲染等非紧急大计算
7. useDeferredValue 是什么?和 useTransition 区别?
答:
- 延迟一个值的更新,等紧急渲染完再更新
- 相当于给值做并发防抖 区别:
useTransition:控制更新动作useDeferredValue:控制值本身- 无法控制更新动作时(如父组件传值)用
useDeferredValue
8. React18 的 Suspense 有哪些能力?
答:
- 服务端:流式 SSR + Suspense 渐进式加载
- 客户端:支持组件懒加载、数据获取的等待状态
- 配合并发渲染,实现不卡顿、可中断的 Loading 切换
Fiber & 架构原理:
9. Fiber 是什么?解决了什么问题?
答:
- Fiber 是一个工作单元 + 虚拟栈帧,是 React16+ 的协调器
- 用链表 + 循环替代递归遍历
- 解决旧版 Stack 协调器同步递归、不可中断、阻塞主线程的卡顿问题
- 实现:可中断、时间切片、优先级调度、双缓存树
10. Render 阶段 和 Commit 阶段 的区别?
答:
- Render 阶段 :遍历 Fiber、diff、标记更新→ 可中断、可异步、可重复执行
- Commit 阶段 :操作 DOM、执行生命周期 / Effect→ 同步、不可中断、必须一次性完成,保证 DOM 稳定。
Fiber 把更新彻底拆成两个阶段:
① Render 阶段(可中断、可异步)
- 执行组件函数 /render
- diff Fiber 树
- 标记增删改
- 可中断、可重启、可重复执行
② Commit 阶段(同步、不可中断)
- 把 WIP 树一次性更新到 DOM
- 执行 useEffect、useLayoutEffect、生命周期
- 必须一次性跑完,保证 DOM 一致性
11. 什么是双缓存树(Current / WIP Tree)?
答:
- Current Tree:当前页面渲染的真实 Fiber 树
- WorkInProgress Tree :内存中计算的新 Fiber 树所有 diff、中断、恢复都在 WIP 树执行,计算完成后一次性替换,保证页面无抖动、稳定渲染。
12. 什么是时间切片(Time Slicing)?
答 :把大渲染任务拆成约 5ms 的小任务片,每执行一片就检查主线程是否有空,没空就主动让出,不阻塞浏览器渲染、交互,保证 60fps 流畅。
13. Lanes 优先级模型是什么?
答 :React18 用位运算 Lanes替代旧优先级,更精细:
- Immediate(同步最高)
- UserBlocking(用户交互)
- Normal(普通更新)
- Low、Idle(低优先级)高优先级可以插队、中断、废弃低优先级任务。
SSR / 服务端:
14. React18 对 SSR 做了什么优化?
答:
- 流式渲染(Streaming SSR):HTML 边渲染边发送,不用等全部数据
- Selective Hydration:选择性注水,只先激活交互组件
- 配合
Suspense,非关键模块延迟加载,首屏速度大幅提升
场景实战题:
15. 输入框搜索大列表卡顿,怎么用 React18 优化?
答 :用 useTransition 把搜索 / 列表渲染标记为非紧急更新:
javascript
const [isPending, startTransition] = useTransition()
const onChange = (e) => {
setInput(e.target.value)
startTransition(() => {
setList(filterBigList(e.target.value))
})
}
输入是高优先级,永远流畅;列表是低优先级,可中断。
16. 自动批处理如何关闭?(少数深挖题)
答 :用 ReactDOM.flushSync() 包裹,强制同步更新:
javascript
flushSync(() => {
setState()
})
总结:
- React18 核心:并发渲染 + createRoot
- 自动批处理:所有场景合并 setState
- 并发三剑客:可中断、优先级、时间切片
useTransition:管动作 ;useDeferredValue:管值- Fiber:链表循环替代递归,实现可中断
- 双缓存:Current/WIP,保证 DOM 稳定
- 两阶段:Render 可中断,Commit 不可中断