前端开发中遇到由于大量计算导致定时器回调不能如期执行,导致页面卡顿的问题,先分析一下思路

解决思路:

解决方案

优点

缺点

优化算法,减少不必要的计算

提高程序员自我修养

算法过于庞大,原作者不在,无法评估工作量

WebWorker 技术,减少 JS 引擎阻塞

实现简单

存在兼容性问题

参考 React Fiber 技术

探索未知领域

实现复杂,存在兼容性问题

最终选择WebWorker 技术方案解决问题,由此涉及一连串的前端知识点

首先我们先看一下基础的概念,引用 ​ ​MDN_Web Workers API​


通过使用Web Workers,Web应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是UI线程)不会因此被阻塞/放慢。


知识点梳理:

  1. 进程和线程区别
  2. 浏览器是多进程的
  3. 浏览器的进程都包含哪些?
  4. 渲染进程中各个线程之间的关系
  5. GUI 渲染线程与 JS 引擎线程互斥
  6. JS 阻塞页面加载
  7. WebWorker 技术

进程和线程的区别

官方的术语:


进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)


术语晦涩难懂,没有计算级基础的同学不容易搞懂,没关系我们看下面的比喻:


进程是一个工厂,工厂有它的独立资源-工厂之间相互独立-线程是工厂中的工人,多个工人协作完成任务-工厂内有一个或多个工人-工人之间共享空间


如果你使用的是 mac 电脑可以在“活动监视”应用程序里面看到进程列表。进程是 cpu 资源分配的最小单位(系统会给它分配内存),而线程是进程里面的“工人”,共享进程资源信息。一个进程下可以有多个线程。

  • 同一进程下的线程可以通信,代价相对较小
  • 不同进程之间也可以通信,代价较大
  • 单线程与多线程,都是指在一个进程内的单和多

浏览器是多进程的

理解过进程与线程之间的关系后,如上图所示,可以得出结论:

  • 浏览器由多进程组成
  • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
  • 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程

浏览器的进程都包含哪些?

Browser 进程:浏览器的主进程,只有一个。负责浏览器界面显示,与用户交互。负责各个页面的管理,创建和销毁其他进程。将Renderer进程得到的内存中的Bitmap,绘制到用户界面上。网络资源的管理,下载等
GPU 进程:最多一个,用于 3D 绘制。我们常说的启动硬件加速渲染使用的进程,就是这个进程
渲染(Renderer)进程:多个,默认每个Tab页为一个渲染进程。其中包含:GUI 渲染线程、js 引擎线程、事件触发线程、定时触发器线程、异步 http 请求线程等
其他进程:如插件进程等

渲染(Renderer)进程中各个线程之间的关系

请牢记,浏览器的渲染进程是多线程的。形象的比喻:浏览器的渲染进程下面有多个工人(线程),一起组成的渲染工厂,实现浏览器的渲染功能。

  1. GUI 渲染线程
  2. 负责渲染浏览器界面,解析HTML、CSS、构建 DOM 树和 RenderObject 树,布局和绘制等
  3. 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
  4. 注意, GUI渲染线程与JS引擎线程是互斥的 ,当JS引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中 等到JS引擎空闲时 立即被执行。
  5. JS 引擎线程
  6. 如V8引擎。JS内核,负责处理 Javascript 脚本程序
  7. JS 引擎负责解析、运行 Javascript 脚本
  8. JS 引擎一直等待着任务队列中任务的到来,然后加以处理
  9. 一个Tab页(renderer进程)中无论什么时候都只有一个 JS 线程
  10. 同样注意, GUI渲染线程与JS引擎线程是互斥的 ,所以如果 JS 执行的时间过长,会导致页面渲染加载阻塞。
  11. 事件触发线程
  12. 可以理解为 JS 引擎事务处理不过来,分出来一部分(事件触发部分),需要浏览器另开一个线程来协助。事件触发线程归属于浏览器而不是JS引擎,用来控制事件循环
  13. 当JS引擎执行代码块如 AJAX异步请求时,会将对应任务添加到事件线程中
  14. 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
  15. 由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理
  16. 定时触发器线程
  17. setInterval与setTimeout所在线程
  18. 浏览器定时计数器并不是由JavaScript引擎计数的 ---- 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确
  19. 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
  20. 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算 4ms
  21. 异步http请求线程
  22. 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
  23. 将检测到状态变更时,如果设置有回调函数,异步线程就 产生状态变更事件 ,将这个回调再放入事件队列中。再由JavaScript引擎执行

JS 阻塞页面加载

由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致。因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起,GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。

一般浏览器的刷新率为60赫兹,相当于 1/60s 刷新一次。这样,我们可以推导出,JS 如果执行时间超过这个时间(1/60s)就会阻塞页面

譬如,假设JS引擎正在进行巨量的计算,此时就算GUI有更新,也会被保存到队列中,等待JS引擎空闲后执行。然后,由于巨量计算,所以JS引擎很可能很久很久后才能空闲,自然会感觉到巨卡无比。

所以,要尽量避免JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

综上所述,我们可以达成共识,就是避免大量的计算阻塞页面的渲染,导致渲染不连贯。

回到我们的主题:Web Worker 究竟是何种骚操作,竟然可以成为前端优化的一种手段?


通过使用Web Workers,Web应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是UI线程)不会因此被阻塞/放慢。


通过上面引用我们知道,当开发人员发现 JS 引擎线程超负荷运作的时候,可以通过Web Worker提供的API开辟一个新的线程,用于独立的运行脚本程序(但是该脚本程序不能操作DOM,主要用于计算),避免 JS 引擎线程阻塞 GUI 线程渲染视图。

以下是 Web Worker 使用最基础的例子,其他更多知识​ ​请参考 Web Worker 使用文档​

// main.js
/*
* @Author: axl
* @Date: 2021-04-16 15:40:28
* @LastEditTime: 2022-05-24 20:43:49
* @LastEditors: axl
*/
const first = document.querySelector('#number1');
const second = document.querySelector('#number2');

const result = document.querySelector('.result');

if (window.Worker) {
const myWorker = new Worker('worker.js');

first.onchange = function () {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
};

second.onchange = function () {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
};

myWorker.onmessage = function (e) {
result.textContent = e.data;
console.log('Message received from worker');
};
} else {
console.log("Your browser doesn't support web workers.");
}
// worker.js
onmessage = function (e) {
console.log('Worker: Message received from main script');
const result = e.data[0] * e.data[1];
if (isNaN(result)) {
postMessage('Please write two numbers');
} else {
const workerResult = 'Result: ' + result;
console.log('Worker: Posting message back to main script');
postMessage(workerResult);
}
};