requestIdleCallback best practice
Record a practice of learning requestIdleCallback api
在学习React的过程中,了解到React Fiber 架构借鉴了requestIdleCallback API实现了强大的调度机制(Scheduler)。将渲染任务拆分成小单元,在浏览器空闲期执行,同时支持任务中断与恢复,并引入任务优先级概念。为此想了解一下 requestIdleCallback(MDN), 并通过一个简单的例子进行实践。
简介
requestIdleCallback
是浏览器提供的一个 API,它允许开发者在浏览器空闲时执行低优先级的任务,而不会影响用户体验。
用一张图来理解:
javascript
requestIdleCallback(callback, options)
callback(deadline)
deadline = {
timeRemaining: () => number //当前浏览器剩余时间(帧剩余时间)
didTimeout: boolean //是否超时
}
options: { timeout: number }
1.requestIdleCallback接受两个参数,
第一个是回调函数,回调函数也就是在当前帧空闲时间要执行的任务。回调函数参数是一个对象,timeRemaining() 用来获取当前值剩余时间 didTimeout 判断是否超时。
第二个参数配置项的参数有 timeout 可配置,因为有些情况下浏览器一直繁忙而无法在指定的时间内得到执行,浏览器将会强制在timeout时间到达时执行该回调。
实践
一个简单的在dom上创建 div的例子, 由于我们在循环里一下子创建了 5w个节点,你会发现页面直接卡死,过了许久之后,才展示出创建的dom。
javascript
const onceBtn = document.getElementById("once");
onceBtn.onclick = () => {
const fragment = document.createDocumentFragment();
for (let i = 0; i < 50000; i++) {
const div = document.createElement("div");
div.textContent = i;
fragment.appendChild(div);
}
container.appendChild(fragment);
};
那么如何用requestIdleCallback这个API进行优化呢?首先我们可以尝试实现一下伪代码:
javascript
//伪代码
const performTask = () => {
const _run2 = () => {
requestIdleCallback(() => {
while (当前还有任务要执行 && 这一帧还有空余时间) {
执行任务;
}
if (当前还有任务要执行) {
_run2();
}
});
};
_run2();
};
很好理解,只要有任务存在就递归的调用requestIdleCallback 来帮我们分批次的处理任务。
代码实现:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="once">同时创建</button>
<button id="batch">分批创建</button>
<div class="container"></div>
<script>
const onceBtn = document.getElementById("once");
const batchBtn = document.getElementById("batch");
const container = document.querySelector(".container");
onceBtn.onclick = () => {
const fragment = document.createDocumentFragment();
for (let i = 0; i < 50000; i++) {
const div = document.createElement("div");
div.textContent = i;
fragment.appendChild(div);
}
container.appendChild(fragment);
};
let idleCallbackId = null;
batchBtn.onclick = () => {
const total = 50000;
let current = 0;
const batchSize = 100;
if (idleCallbackId !== null) {
cancelIdleCallback(idleCallbackId);
}
const _run = () => {
idleCallbackId = requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && current < total) {
const fragment = document.createDocumentFragment();
const end = Math.min(current + batchSize, total);
for (let i = current; i < end; i++) {
const div = document.createElement("div");
div.textContent = i;
fragment.appendChild(div);
}
container.appendChild(fragment);
current = end;
}
if (current < total) {
_run();
}
});
};
_run();
};
</script>
</body>
</html>
由于在空闲时间执行并且将任务分批次执行,我们可以看到页面上直接创建出了div。由于大量的dom创建,页面还是会卡顿的。这个例子只是用来参考。
最终效果
其实这个例子是不太合适的,requestIdleCallback 在性能优化方面非常有用,但也需要适合的场景比如:
- ✅ 低优先级的后台任务
- ✅ 可以分批执行的工作
- ✅ 不紧急的 UI 更新
希望大家在未来项目性能优化方面可以想到它。
