那些被忽略但很好用的 Web API / RequestIdleCallback

时间管力大师就是要忙里偷闲

各位应该知道 JavaScript 是单执行绪(单线程)的程序语言,也就是一次只能处理一件事情。这样的特性会使得事件的执行必定有个先後顺序,这时候就会希望重要的事情能够排序在前面,剩下比较不重要的任务等空闲时再处理即可,这时候就可以靠 RequestIdleCallback 来帮助我们。


RequestIdleCallback

RequestIdleCallback 会在浏览器「每一帧」中剩下的空闲里来执行当中的 Callback。

我们之前在介绍 RequestAnimationFrame 时有提过「帧数(FPS)」的概念,也就是「一秒钟内能够更新多少帧」,假如在一秒内能够更新 60 帧,则 FPS 为 60,每一帧的时间约为 16.7 ms(毫秒)。

对於浏览器来说每一次「重绘(Repaint)」就是「一帧」,而这一帧要花多少时间就要看当下的网路或硬体状况而定了。在这每一帧中,浏览器都有可能正在执行任务,若这个任务完成时,当下那一帧还没结束时,就会有一个短暂的空闲时间。

以 60FPS 为例,每一帧的空闲时间必定小於等於 16.7 ms。

而只要有这个空闲时间 RequestIdleCallback 就会去执行当中的 Callback,来完成那些我们觉得不重要的任务,换句话说,如果浏览器一直处於繁忙状态的话,那该任务就会一直无法执行。

# Window.requestIdleCallback

requestIdleCallback 有两个参数要传入:

  • callback: 需要在空闲时间(Idle)执行的函示。
  • timeout: 这是一个可选参数,你可以设定一个时间来强制执行 callback,以避免浏览器因为持续繁忙的忽略(单位:毫秒)。

大部分情况不建议使用 timeout,因为会使用 requestIdleCallback 就代表不想影响主线程的任务进行 。

const handlerId = requestIdleCallback(function () {
  //..做些不重要的事
}, 500);
cancelIdleCallback(handlerId); // 取消requestIdleCallback

 

# IdleDeadline

而我们传入的 Callback Function 会被丢进一个由 requestIdleCallback 提供的参数,该参数通常取名为 deadline,并且有两个属性可以使用:

  • didTimeout: 这是一个唯读属性,以布林值来表示 Callback 是否是因为 timeout 被触发的。
  • timeRemaining: 它是一个 method,执行後会传传一个毫秒数,用来表示当下这一帧的剩余时间。
requestIdleCallback(function (deadline) {
  // 如果你在 requestIdleCallback 中没有传入 timeout 参数,didTimeout 必定为 false
  console.log(deadline.didTimeout);
  console.log(deadline.timeRemaining());
}, 500);

 

# 实际测试

由於 JavaScript 是单执行绪,所以要是我今天进行了一个需要耗费大量时间的任务,那使用者的 UI 操作其实也会受到影响。
就像下面这个范例中,在 count 被函式 add 加到 1000000 以前,你不管怎麽敲击键盘,keydown 事件都不会被触发,因为浏览器正在忙着算数:

window.addEventListener("keydown", function () {
  console.log("Hey !!!!!!!!!");
});

let count = 0;
add();
function add() {
  if (count < 10000) {
    console.log(count++);
    add();
  }
}

但是我们只要用 requestIdleCallback 来改写一下,那状况就不一样了,因为这时候 add 这项任务的优先度会往後排,所以当我按下键盘时,浏览器会先处理 keydown 事件,等到闲置下来後才会继续进行。

window.addEventListener("keydown", function () {
  console.log("Hey !!!!!!!!!");
});

let count = 0;
requestIdleCallback(add);
function add(deadline) {
  if (deadline.timeRemaining() > 0) {
    if (count < 10000) {
      console.log(count++);
      requestIdleCallback(add);
    }
  }
}

 

# 使用情境

在了解 RequestIdleCallback 的效果後,我第一个想到的实际应用会是 LazyLoad,想像以下,如果我们有个网页,当中有几十甚至几百张的高画质图片需要显示,可想而知浏览器的负担会相当的大,非常有可能会影响页面的效能与任务执行,但如果们我利用 requestIdleCallback 来处理,就可以在不影响主执行绪的情况下载入图片。

const images = [
  "https://img/001.png",
  "https://img/002.png",
  //.....
  "https://img/099.png",
  "https://img/100.png",
];

requestIdleCallback(loadImage);
function loadImage(deadline) {
  if (deadline.timeRemaining() > 0) {
    if (images.length) {
      const imgSrc = images.shift();
      const img = new Image(250, 150);
      img.onload = document.body.appendChild(img);
      img.src = imgSrc;
      requestIdleCallback(loadImage);
    }
  }
}

 

不晓得使用过 React 的朋友有没有了解过 React Fiber 呢?其实它的原理就和 RequestIdleCallback 一样,将大量没那麽优先的工作拆成许多小片段,在琐碎的时间里慢慢完成,也因为这样的机制,使得我们可以去中断它,将一些突发的重要任务(例如使用者的 UI 事件)插在这些小片段中,宛如有另一条执行绪一般。


<<:  Day18 Web Server 相关扫描

>>:  [Day18] Null byte Injection

DAY 22-凭证颁发机构CA

「子非鱼,安知鱼之乐。」 在介绍协定之前, 我们要来介绍一个非常重要的概念,叫做凭证颁发机构(Cer...

Day17:比大小

记得初学Java的时候,若要对List进行排序,可以使用Collections的静态方法sort()...

Day 7— 自动化回信机(4) 勾选後寄出通知信

我们已经顺利的将 onEdit(e) 以及 MailApp.sendMail(message) 学完...

Day 11-Atlantis 做 Terraform Remote Plan & Remote Apply

使用 atlantis 做 terraform automation,Terraform Remot...

D13 删除特定的使用者文件

已经先有测试资料了 来试试看删除文件的方法 doc_info/views.py 一样使用修饰器来验证...