- JavaScript执行时,js引擎和页面渲染引擎在同一个渲染线程,GUI渲染和JavaScript执行两者互斥。
如果js执行任务事件过程,则会影响浏览器的渲染引擎,造成页面卡顿。
起因
目前主流设备的屏幕刷新率为60次/秒
- 每秒绘制的帧数(FPS)达到 60 页面显示才流畅;小于这个值时,用户会感觉到卡顿;
- 每帧的预算时间是16.667ms
- 每帧的开始包括样式计算、布局、绘制
- JavaScript执行和页面渲染在同一个渲染进程中,GUI渲染和JavaScript执行两者互斥
- 如果js任务执行时间过 长,浏览器渲染会被推迟。
浏览器一秒渲染60帧画面,每帧显示时间在16.66ms。浏览器在绘制每帧时经历的过程,如下图:
rAF【requestAnimationFrame】回调函数
- requestAnimationFrame回调函数在绘制之前执行。
- 每帧绘制时都会执行
- requestAnimationFrame(callback) 的 callback回调函数中的参数timestamp是回调被调用的时间,也就是当前帧的起始时间
rafTime 等于 performance.timing.navigationStart + performance.now ; 约等于Date.now();
<body><div id="box"style="background-color: red;width: 1px; height: 20px;color: antiquewhite;"></div><button>start</button><script>let div = document.querySelector("#box");let button = document.querySelector("button");let startTime;function progress() {div.style.width = div.offsetWidth + 1 + "px";div.innerHTML = div.offsetWidth + "%";if (div.offsetWidth < 100) {startTime = Date.now();requestAnimationFrame(progress);}}button.onclick = function () {div.style.width = 0;startTime = Date.now();// 点击按钮时,调用一次requestAnimationFrame(progress);};</script></body>
requestIdleCallback
requestIdleCallback 在每帧绘制完成后如果有剩余时间才执行,而不影响用户交互的关键事件,如动画、窗口缩放和输入响应
- 正常一帧任务完成后,时间小于16.6ms还有剩余时间,就会执行 requestIdleCallback 里面的任务。
- requestAnimationFrame 的回调在每帧时必定执行,属于高优先级任务;requestIdleCallback 回调则不能保证每帧都执行,属于低优先级任务。

function sleep(delay) {for (let now = Date.now(); Date.now() - now < delay; ) {}}let allStart = 0;const works = [() => {allStart = Date.now();console.log("this first work start");sleep(20);console.log("this first work end");},() => {console.log("this second work start");sleep(15);console.log("this second work end");},() => {console.log("this third work start");sleep(10);console.log("this third work end");console.log(Date.now() - allStart);},];requestIdleCallback(workLoop, { timeout: 1000 });// deadline参数是一个对象,有2个属性// timeRemaining(), 返回此帧还剩下多少时间让用户使用// didTimeout 判断回调任务callback是否超时function workLoop(deadline) {console.log(`this frame remainTime is ${deadline.timeRemaining()}`);// deadline.timeRemaining() > 0 说明还有剩余时间// deadline.didTimeout说明任务已经过期,则必须执行while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&works.length > 0) {performUnitOfWork();}if (works.length > 0) {console.log(`this frame remainTime is ${deadline.timeRemaining()},时间片已经到期,等待下次调度`);window.requestIdleCallback(workLoop, { timeout: 1000 });}}function performUnitOfWork() {works.shift()();}
MessageChannel
- 上面的 requestIdleCallback 方法只有Chrome支持
- react利用 MessageChannel 模拟 requestIdleCallback ,将回调延迟到绘制操作之后
- MessageChannel 允许创建一个新的消息通道,并通过它的2个 MessagePort 属性发送数据
- MessageChannel 创建一个通信管道,这个管道有2个端口,每个端口都可以通过 postMessage 发送数据,另一个端口只要绑定了onmessage回调方法,就可以通过另一个端口传递数据
MessageChannel 是宏任务
<script>let channel = new MessageChannel();let port1 = channel.port1;let port2 = channel.port2;port1.onmessage = function (event) {console.log("port1接收的数据:", event.data);};port2.onmessage = function (event) {console.log("port2接收的数据:", event.data);};port1.postMessage('port1发送数据');port2.postMessage('port2发送数据');</script>
模拟实现 原生的requestIdleCallback
let channel = new MessageChannel();let activeFrameTime = 1000 / 60;let frameDeadline; // 每帧的截止时间let pendingCallback;let timeRemaining = () => frameDeadline - performance.now();channel.port2.onmessage = function () {let currentTime = performance.now();// 如果帧的截止时间,小于当前时间,说明已经过期let didTimeout = frameDeadline <= currentTime;if (didTimeout || timeRemaining() > 0) {if (pendingCallback) {// 原生requestIdleCallback回调函数中的2个参数pendingCallback({ didTimeout, timeRemaining });}}};window.requestIdleCallback = (callback, options) => {requestAnimationFrame((rafTime) => {// rafTime帧的开始时间console.log("rafTime", rafTime);frameDeadline = rafTime + performance.now();pendingCallback = callback;channel.port1.postMessage("send");});};
Fiber是什么
通过某些调度策略合理分配CPU资源,提高用户的响应速度。
- 通过 Fiber 架构,react把协调组件数据变化的过程变成可被中断,在适当的时候让出GPU执行权,就可以让浏览器及时处理用户的交互。
Fiber是一个执行单元
- Fiber是一个执行单元,react在检查现在还剩下多少时间,如果没有时间交出控制权。
Fiber是一种数据结构
- react目前的做法使用链表,每个虚拟节点内部表示为Fiber.
Fiber执行阶段
每个渲染节点有2个阶段:Reconciliation(协调render阶段)和Commit(提交阶段)
- Reconciliation阶段:可以认为是diff过程,这个阶段可以被中断。在该阶段找出所有节点变更,例如节点新增、删除、属性变更等,这些称为react的 副作用(effect)
Commit阶段:将上个阶段计算出来的需要处理的副作用一次执行。这个阶段的执行必须同步执行,不能被打断。
render阶段
从顶点开始遍历
- 如果有儿子,先遍历第一个儿子
- 如果没有儿子,标志着此节点遍历完成
- 如果有兄弟节点,继续遍历兄弟节点
- 如果没有下一个兄弟,返回父节点并标识 父节点遍历完成;如果有叔叔节点,则遍历叔叔节点;没有叔叔节点,则父节点上一个节点遍历结束。整个流程遍历完成

/*Fiber是一个执行单元1. Fiber是一个执行单元,类似对象。每次执行完一个执行单元,react会检查可使用的时间还有多少,如果没时间就把控制权让出去2. 通过Fiber架构,让Reconcilation过程变成可被中断,适时会让出CPU执行权,让浏览器优先处理用户的交互Fiber执行阶段每次渲染包含2个阶段:Reconcilation(协调或render渲染)和Commit(提交阶段)协调阶段:Reconcilation可以认为是diff阶段,该阶段可被中断,会造成节点变更,如新增、删除等,这些变更react称为副作用effect提交阶段:将上一个阶段计算出来的所有需要处理的副作用一次性执行。这个阶段必须同步执行,不能被打断*/let A1 = { type: "div", key: "A1" };let B1 = { type: "div", key: "B1", return: A1 };let B2 = { type: "div", key: "B2", return: A1 };let C1 = { type: "div", key: "C1", return: B1 };let C2 = { type: "div", key: "C2", return: B1 };A1.child = B1;B1.sibling = B2;B1.child = C1;C1.sibling = C2;let nextUnitOfWork; // 下一个执行单元function workLoop() {while (nextUnitOfWork) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);}if (!nextUnitOfWork) {console.log("render阶段结束");}}function performUnitOfWork(fiber) {beginWork(fiber);if (fiber.child) {//如果有子,返回第一个子节点 A1之后B1return fiber.child;}while (fiber) { // 如果没有子,说明该节点完成。completeUnitOfWork(fiber);// 节点自身完成,开始查看是否有兄弟节点,如果有返回兄弟节点if (fiber.sibling) {return fiber.sibling;}// 兄弟节点也遍历完毕,则返回父节点,让父节点再次查找兄弟节点,即该节点的叔叔节点。fiber = fiber.return;}}function completeUnitOfWork(fiber) {console.log(fiber.key, "end");}function beginWork(fiber) {console.log(fiber.key, "start");}nextUnitOfWork = A1;workLoop();
