前端性能优化之WebWorker
前端开发中遇到由于大量计算导致定时器回调不能如期执行,导致页面卡顿的问题,先分析一下思路
解决思路:
解决方案 | 优点 | 缺点 |
---|---|---|
优化算法,减少不必要的计算 | 提高程序员自我修养 | 算法过于庞大,原作者不在,无法评估工作量 |
WebWorker 技术,减少 JS 引擎阻塞 | 实现简单 | 存在兼容性问题 |
参考 React Fiber 技术 | 探索未知领域 | 实现复杂,存在兼容性问题 |
最终选择WebWorker 技术方案解决问题,由此涉及一连串的前端知识点
首先我们先看一下基础的概念,引用 MDN_Web Workers API
通过使用Web Workers,Web应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是UI线程)不会因此被阻塞/放慢。
知识点梳理:
- 进程和线程区别
- 浏览器是多进程的
- 浏览器的进程都包含哪些?
- 渲染进程中各个线程之间的关系
- GUI 渲染线程与 JS 引擎线程互斥
- JS 阻塞页面加载
- WebWorker 技术
进程和线程的区别
官方的术语:
进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
术语晦涩难懂,没有计算级基础的同学不容易搞懂,没关系我们看下面的比喻:
进程是一个工厂,工厂有它的独立资源-工厂之间相互独立-线程是工厂中的工人,多个工人协作完成任务-工厂内有一个或多个工人-工人之间共享空间
如果你使用的是 mac 电脑可以在“活动监视”应用程序里面看到进程列表。进程是 cpu 资源分配的最小单位(系统会给它分配内存),而线程是进程里面的“工人”,共享进程资源信息。一个进程下可以有多个线程。
- 同一进程下的线程可以通信,代价相对较小
- 不同进程之间也可以通信,代价较大
- 单线程与多线程,都是指在一个进程内的单和多
浏览器是多进程的
理解过进程与线程之间的关系后,如上图所示,可以得出结论:
- 浏览器由多进程组成
- 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
- 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程
浏览器的进程都包含哪些?
Browser 进程:浏览器的主进程,只有一个。负责浏览器界面显示,与用户交互。负责各个页面的管理,创建和销毁其他进程。将Renderer进程得到的内存中的Bitmap,绘制到用户界面上。网络资源的管理,下载等
GPU 进程:最多一个,用于 3D 绘制。我们常说的启动硬件加速渲染使用的进程,就是这个进程
渲染(Renderer)进程:多个,默认每个Tab页为一个渲染进程。其中包含:GUI 渲染线程、js 引擎线程、事件触发线程、定时触发器线程、异步 http 请求线程等
其他进程:如插件进程等
渲染(Renderer)进程中各个线程之间的关系
请牢记,浏览器的渲染进程是多线程的。形象的比喻:浏览器的渲染进程下面有多个工人(线程),一起组成的渲染工厂,实现浏览器的渲染功能。
-
GUI 渲染线程
-
负责渲染浏览器界面,解析HTML、CSS、构建 DOM 树和 RenderObject 树,布局和绘制等
-
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
-
注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
-
JS 引擎线程
-
如V8引擎。JS内核,负责处理 Javascript 脚本程序
-
JS 引擎负责解析、运行 Javascript 脚本
-
JS 引擎一直等待着任务队列中任务的到来,然后加以处理
-
一个Tab页(renderer进程)中无论什么时候都只有一个 JS 线程
-
同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果 JS 执行的时间过长,会导致页面渲染加载阻塞。
-
事件触发线程
-
可以理解为 JS 引擎事务处理不过来,分出来一部分(事件触发部分),需要浏览器另开一个线程来协助。事件触发线程归属于浏览器而不是JS引擎,用来控制事件循环
-
当JS引擎执行代码块如 AJAX异步请求时,会将对应任务添加到事件线程中
-
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
-
由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理
-
定时触发器线程
-
setInterval与setTimeout所在线程
-
浏览器定时计数器并不是由JavaScript引擎计数的 ---- 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确
-
因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
-
注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算 4ms
-
异步http请求线程
-
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
-
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由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);
}
};