本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言
如果生活存在奇迹,那一定是努力的轨迹!
hello,小伙伴们好呀,我是小羽同学~
距上次发布的《项目没亮点?那就来学下web worker吧~》,有很多的小伙伴都说学会了,但是在实际的项目中却好像没有什么实战的场景
,毫无用武之地呀。
emmm,如果没有深入的去思考项目的可优化点的时候,的确会有这种想法。
但是问题不大,小羽将在这篇文章中举两个
简单易懂,并且是可以有机会应用到大家项目中的例子
!!!
尤其是做后台管理系统
的同学,不要怕你们的项目没有亮点,你们也可以大展身手
啦~
查漏补缺
上期的《项目没亮点?那就来学下web worker吧~》一文中说漏了一些相关的知识点
,然后在评论区里面也有小伙伴们讨论了,小羽自己也经过一番查漏补缺
后,在这里先简单的做一个总结,如果有错误的地方小伙伴们可以及时指出。
浏览器中会存在如下这些进程
- 浏览器主进程:负责协调、主控、只有一个
- GPU进程:用于3D绘制,可以禁用,只有一个
- 第三方进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
- 渲染进程:浏览器渲染进程(render进程),即通常说的浏览器内核,主要作用是:页面渲染、脚本执行、事件处理等。每一个标签页的打开都会创建一个Render进程,并且互不影响。默认的话一个标签页对应一个Render进程,但是,有时候浏览器会将多个进程合并,如打开了多个空白标签页。
此外还会存在很多线程
,如:
- GUI渲染线程:主要负责渲染浏览器界面,解析HTML,CSS,构建DOM树和Render树,布局和绘制、以及回流重绘等。
- JS引擎线:JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码。
- 事件触发线程:事件触发线程归属于浏览器,而不是属于JS引擎,JS引擎处理的事务过多,需要浏览器另开线程来进行协助
- 定时器触发线程:即setInterval与setTimeout所在线程
- 异步http请求线程:XMLHttpRequest连接后通过浏览器新开一个线程请求
- worker线程
其中GUI渲染线程和JS引擎线程是互斥
的,他们会共用渲染进程
,当JS引擎执行时,GUI线程就会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时,才会被执行。
举一个简单的小例子,比如咱们通过requestAnimationFrame
这个api每16.6ms获取一次时间,并且渲染。当JS引擎线程繁忙
的时候,GUI线程就会被挂起
,即不会更新
咱们页面上的时间,从而导致卡顿
的现象。
Excel大文件导出
如果小伙伴们做的项目是普普通通的后台管理系统
,那么我强烈推荐
你看一下这一小节。
因为做后台系统的同学肯定有做过table表格
相关的需求。而当一个后台系统存在table表格时,那么它很大概率还会伴随着导出表格
的功能。
所以我不相信没有,没有的同学骑上你们的小电驴去,提起40米的大刀,去找你们的产品经理吧。
首先感谢这位同学,让我想到了web worker在后台管理系统
中存在这么一个通用的解决方案
。
那咱们先简单说一下主线程导出
和worker线程
导出的逻辑吧,为了减少网络等因素的影响
,咱们将所有的表格数据在初始化前就构造好了假数据,仅处理excel导出时
的逻辑
。
- 主线程导出:通过
exceljs
构建表格相关的参数,传入相关的数据,然后转换为blob流
,最后通过file-save导出
- worker线程导出:通过
ExcelWokrer
创建worker线程
,通过postmessage
向worker线程传递相应的excel数据,在worker线程中通过exceljs
构建表格相关数据,然后转换为blob流
,接着将生成的blob流通过postmessage
传回来主线程
,最后通过file-save
导出 - 时间戳渲染:时间戳渲染主要是用来测试咱们的JS引擎线程是否
繁忙
,从而导致GUI线程被挂起
,从而导致卡顿
的现象。
// index.tsx
import { Button, Table } from 'antd';
import React, { useState, useEffect } from 'react';
import ExcelJS from 'exceljs';
import FileSaver from 'file-saver';
import ExcelWorker from './excel.worker?worker';
const dataSource = [];
for (let i = 1; i < 30000; i++) {
dataSource.push({
key: i,
name: `name-${i}`,
age: 18,
tag: '小羽同学',
value1: `value1-${i}`,
value2: `value2-${i}`,
value3: `value3-${i}`,
});
}
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
},
{
title: '标签',
dataIndex: 'tag',
key: 'tag',
},
{
title: 'value1',
dataIndex: 'value1',
key: 'value1',
},
{
title: 'value2',
dataIndex: 'value2',
key: 'value2',
},
{
title: 'value3',
dataIndex: 'value3',
key: 'value3',
},
];
export default function Excel() {
const [showTime, setShowTime] = useState(Date.now());
useEffect(() => {
updateShowTime();
}, []);
const updateShowTime = () => {
setShowTime(Date.now());
requestAnimationFrame(updateShowTime);
};
// 主线程导出Excel
const mainExportExcel = () => {
// 创建工作簿
const workbook = new ExcelJS.Workbook();
// 添加工作表
const worksheet = workbook.addWorksheet('sheet1');
// 设置表格内容
const _titleCell = worksheet.getCell('A1');
_titleCell.value = 'Hello ExcelJS!';
const workBookColumns = columns.map((item) => ({
header: item.title,
key: item.key,
width: 32,
}));
worksheet.columns = workBookColumns;
worksheet.addRows(dataSource);
// 导出表格
workbook.xlsx.writeBuffer().then((buffer) => {
let _file = new Blob([buffer], {
type: 'application/octet-stream',
});
FileSaver.saveAs(_file, 'ExcelJS.xlsx');
});
};
// 子线程导出Excel
const workerExportExcel = async () => {
const _file = await new Promise((resolve, reject) => {
const myWorker = new ExcelWorker();
myWorker.postMessage({
columns,
dataSource,
});
myWorker.onmessage = (e) => {
resolve(e.data.data); // 关闭worker线程
myWorker.terminate();
};
});
FileSaver.saveAs(_file, 'ExcelJS.xlsx');
};
return (
<div>
<Button onClick={mainExportExcel}>导出全部</Button>
<Button onClick={workerExportExcel}>worker导出全部</Button>
<span>{showTime}</span>
<Table dataSource={dataSource} columns={columns} />
</div>
);
}
复制代码
// excel.worker.ts
import ExcelJS from 'exceljs';
// onmessage事件
onmessage = function (e) {
const {
data: { columns, dataSource },
} = e;
// 创建工作簿
const workbook = new ExcelJS.Workbook();
// 添加工作表
const worksheet = workbook.addWorksheet('sheet1');
// 设置表格内容
const _titleCell = worksheet.getCell('A1');
_titleCell.value = 'Hello ExcelJS!';
const workBookColumns = columns.map((item) => ({
header: item.title,
key: item.key,
width: 32,
}));
worksheet.columns = workBookColumns;
worksheet.addRows(dataSource);
// 导出表格
workbook.xlsx.writeBuffer().then((buffer) => {
let _file = new Blob([buffer], {
type: 'application/octet-stream',
});
// 将获取的数据通过postMessage发送到主线程
self.postMessage({
data: _file,
name: 'worker test',
});
self.close();
});
};
复制代码
主线程
导出3w数据量
的excel表格如下图,从中可以明显的发现咱们的时间戳渲染有明显的卡顿现象
worker线程
导出3w数据量
的excel表格如下图,从中可以发现咱们的时间戳渲染并无明显
的卡顿现象
,还是照常渲染。
离屏canvas
在说下一个例子之前,咱们先补充一个新的知识点。
离屏canvas
(OffscreenCanvas) 是一个实验中的新特性,主要用于提升 Canvas 2D/3D 绘图的渲染性能和使用体验。
OffscreenCanvas
和canvas
都是渲染图形的对象。 不同的是canvas只能在window环境下使用,而OffscreenCanvas即可以在window环境下使用,也可以在web worker中使用,这让不影响浏览器主线程的离屏渲染
成为可能。
它的兼容性如下图,可以发现兼容了最新的chrome
、edge
以及firefox
,但不兼容safiri
和ie浏览器
,所以项目需要兼容这两种浏览器的小伙伴们可以溜了~
图片批量压缩
小伙伴们在项目中应该都会有做过图片上传
的需求吧?
但是如果我们直接传原图的话,会很大,不仅占用用户的流量
,而且还会占用oss/cdn/服务器
的容量
,这样子对用户和公司来说都是不负责的行为吧,所以在上传图片之前,咱们通常都会对图片进行压缩
。而图片压缩,在前端通常则是使用canvas
。
单个图片直接在主线程进行压缩没什么问题,但是如果需要上传100张图片
,并且在上传之前需要对这100张图片进行压缩
的话。此时,对咱们的主线程
就会有比较大的压力
了。因此,在数量较大的情况下,建议尝试使用woker线程
对图片进行压缩
,从而降低
咱们主线程的压力。
对了,这里咱们需要注意的是,在worker中
是没有dom
的,所以咱们就无法通过domcument.createElement('canvas')
来创建canvas
,需要用到的是离屏canvas
。但离屏canvas中是没有toDataUrl
对象的,需要先转blob后,才可以转base64(当然,上传文件咱们一般都是传blob)
- 主线程压缩:加载图片,创建canvas,在canvas中绘制图片,通过
toDataUrl压缩
并生成base64 - worker线程压缩:通过
fetch加载图片
并转换为blob流,创建多个worker线程
,然后将图片的blob流
组成数组
后通过postmessage
传给worker线程,然后创建离屏canvas
,在离屏canvas中绘制
图片,通过convertToBlob
将图片压缩并生成blob流
,接着通过FileReader
将blob流转换为图片后,最后通过postmessage传递回来主线程
- 时间戳渲染:时间戳渲染主要是用来测试咱们的JS引擎线程是否
繁忙
,从而导致GUI线程被挂起
,从而导致卡顿
的现象。
// index.tsx
import { Button, Table } from 'antd';
import React, { useState, useEffect } from 'react';
import ComporessWorker from './compress.worker?worker';
const allImgNum = 100;
const url =
'https://pic3.zhimg.com/v2-58d652598269710fa67ec8d1c88d8f03_r.jpg?source=1940ef5c';
export default function Home() {
const [showTime, setShowTime] = useState(Date.now());
useEffect(() => {
updateShowTime();
}, []);
const updateShowTime = () => {
setShowTime(Date.now());
requestAnimationFrame(updateShowTime);
};
// 主线程压缩图片
const mainCompressImg = async () => {
const res = await new Promise((resolve, reject) => {
const img = new Image();
img.src = url;
img.setAttribute('crossOrigin', 'Anonymous');
img.onload = () => {
resolve(img);
};
img.onerror = (e) => {
reject(e);
};
});
// console.log(res);
console.time('compress');
for (let i = 0; i < allImgNum; i++) {
const compressRes = compressImg(res);
// console.log(compressRes);
}
console.timeEnd('compress');
return res;
};
const compressImg = (img) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL('image/jpeg', 0.75);
};
// 子线程压缩图片
const workerCompressImg = async () => {
const res = await fetch(url).then((res) => res.blob());
const workerList = [[], [], [], [], []];
for (let i = 0; i < allImgNum / 5; i++) {
workerList[0].push(res);
workerList[1].push(res);
workerList[2].push(res);
workerList[3].push(res);
workerList[4].push(res);
}
console.time('compressWorker');
const pList = [];
for (let item of workerList) {
const compressP = new Promise((resolve, reject) => {
const myWorker = new ComporessWorker();
myWorker.postMessage({
imageList: item,
});
myWorker.onmessage = (e) => {
resolve(e.data.data);
};
});
pList.push(compressP);
}
const pRes = await Promise.all(pList);
console.log(pRes);
console.timeEnd('compressWorker');
};
return (
<div>
<Button onClick={mainCompressImg}>压缩图片</Button>
<Button onClick={workerCompressImg}>worker压缩图片</Button>
<span>{showTime}</span>
</div>
);
}
复制代码
// compress.worker.ts
// onmessage事件
onmessage = async function (e) {
const {
data: { imageList },
} = e;
const resList = [];
for (let img of imageList) {
// @ts-ignore
const offscreen = new OffscreenCanvas(100, 100);
const ctx = offscreen.getContext('2d');
const imgData = await createImageBitmap(img);
offscreen.width = imgData.width;
offscreen.height = imgData.height;
ctx.drawImage(imgData, 0, 0, offscreen.width, offscreen.height);
const res = await offscreen
.convertToBlob({ type: 'image/jpeg', quality: 0.75 })
.then((blob) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
return new Promise((resolve) => {
reader.onloadend = () => {
resolve(reader.result);
};
});
});
resList.push(res);
}
self.postMessage({
data: resList,
name: 'worker test',
});
self.close();
};
复制代码
主线程对同一张图片压缩图片100次
的结果如下图,可以发现主线程在压缩时压力较大
,将GUI渲染线程挂起
,导致页面卡顿
的现象发生,并且整体的图片压缩时间为108s
创建5个worker线程
,并对同一张图片压缩100次
(每个worker线程压缩20次),可以发现此时咱们的GUI线程还是在继续渲染
,未被js脚本阻塞
,并且整体的图片压缩时间也下降到了37s
小结
本文主要是针对
小伙伴们提出的:很难在日常的开发中
使用到worker线程的问题。
介绍了两个可以应用在日常开发中的场景——excel大文件导出
和图片批量压缩
。希望可以帮助到一些感觉自己项目缺少亮点
的同学,以及遇到类似问题时,可以有多一种解决的思路
。
此外,还对上篇文章中的缺失的浏览器进程和线程
部分内容进行了一些补充,以及介绍了离屏canvas
~
如果看这篇文章后,感觉有收获的小伙伴们可以点赞
+收藏
哦~
如果想和小羽
交流技术可以加下wx,也欢迎小伙伴们来和小羽唠唠嗑
,嘿嘿~