安装loader
npm install worker-loader -D
如果直接把worker.js放到public目录下,则不需要安装loader
vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave:false, // 关闭eslint检查
chainWebpack: config => {
config.module
.rule('worker')
.test(/\.worker\.js$/) // 文件名必须要xxx.worker.js
.use('worker')
.loader('worker-loader')
}
})
HelloWorld.vue
<template>
<div class="hello">
<button @click="useWorker">calculate</button>
{{ result }}
</div>
</template>
<script>
import Worker from './demo.worker.js'; // this.worker = new Worker(); 方式才需要引入
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
result: 0,
myworker: null
}
},
methods: {
useWorker() {
this.worker.postMessage('帮我计算1+1等于多少')
}
},
mounted() {
// this.worker = new Worker('worker.js', { name : 'myWorker' }); 读取public目录下得js文件,可以配置名字,简单粗暴,不需要worker-loader
this.worker = new Worker(); // 使用上面import进来的js,名字为 demo.worker.worker.js,不可配置,路径相对比较灵活,需要worker-loader
// worker的名字,主要在谷歌浏览器 - 控制台 - sources 中体现
this.worker.onmessage = event => {
this.result = event.data;
console.log('主线程收到回复,即将关闭worker连接');
// this.worker.terminate();
}
},
beforeDestroy() {
this.worker?.terminate()
}
}
</script>
demo.worker.js
// self.addEventListener('message', function (e) {
// console.log(e.data);
//
// postMessage('等于2');
//
// // close();
// }, false)
// this.addEventListener('message', function (e) {
// console.log(e.data);
//
// postMessage('等于2');
//
// close();
// }, false)
onmessage = function(e) {
console.log(e.data);
postMessage('等于2');
// close();
}
应用场景
- 浏览器的JS线程和GUI渲染线程互斥
- 在主线程跑吃性能的同步任务,GUI渲染线程会挂起,页面不能及时响应渲染
- 在worker跑时,GUI渲染线程不会被挂起,页面可以正常响应
优势:
- 避免页面渲染阻塞。用一个worker处理主线程的任务,两者处理时间差不多,worker的优势在于处理过程中不会影响页面的渲染(e.g. 导出时 Element.message组件弹出提示,但是由于JS处理时间过长,导致页面渲染被阻塞,提示不能及时显示)
- 减少任务处理时间。worker可以有多个(多线程),用多个worker处理主线程的任务时,总的任务时长会减少(e.g. 压缩100张图片)
处理CPU密集型同步任务
// HelloWorld.vue
<template>
<div class="hello">
<p>时间戳:{{ timeStamp }}</p>
<button @click="useWorker">worker运算</button>
<button @click="calc">主线程运算</button>
<div>计算从 1 到给定数值的总和</div>
<input type="number" :value="1000000000" readonly />
<p>计算结果为:{{ result || '-' }}</p>
</div>
</template>
<script>
import Worker from './demo.worker.js';
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
result: 0,
timeStamp: 0,
}
},
methods: {
useWorker() {
console.time('worker运算');
this.worker.postMessage({
command: 'calc',
})
},
calc() {
let result = 0
console.time('主线程运算')
// 计算求和(模拟复杂计算)
for (let i = 0; i <= 1000000000; i++) {
result += i
}
// 由于是同步计算,在没计算完成之前下面的代码都无法执行
this.result = result;
console.timeEnd('主线程运算')
}
},
mounted() {
this.worker = new Worker();
this.worker.onmessage = event => {
this.result = event.data;
console.timeEnd('worker运算')
console.log('主线程收到回复,即将关闭worker连接');
this.worker.terminate();
}
this.timer = setInterval(() => {
this.timeStamp = Date.now();
})
},
beforeDestroy() {
this.worker?.terminate()
clearInterval(this.timer)
}
}
</script>
// demo.worker.js
onmessage = function(e) {
console.log(e.data);
switch (e.data.command) {
case 'calc':
calc();
break;
}
};
function calc() {
let result = 0
// 计算求和(模拟复杂计算)
for (let i = 0; i <= 1000000000; i++) {
result += i
}
// 由于是同步计算,在没计算完成之前下面的代码都无法执行
postMessage(result);
}
// 控制台输出如下
主线程运算: 1063.4140625 ms
worker运算: 1079.2041015625 ms
导出10万条数据的表格
<template>
<div class="hello">
{{ timeStamp }}
<button @click="getBlob">主线程导出表格</button>
<button @click="useWorker">worker导出表格</button>
</div>
</template>
<script>
import Worker from './demo.worker.js';
import { saveAs } from "file-saver";
import Excel from "exceljs";
export default {
name: 'ExportExcel',
props: {
msg: String
},
data() {
return {
timeStamp: 0,
timer: null,
blobData: null
}
},
methods: {
useWorker() {
console.time('worker导出')
this.worker.postMessage({
// 不能传方法
// fn: () => this.exportExcel(true)
// fn: () => {}
command: 'getBlob',
})
},
getBlob() {
console.time('主线程导出')
const workbook = new Excel.Workbook();
const worksheet = workbook.addWorksheet('My Sheet');
worksheet.columns = [
{ header: '序号', key: 'index', width: 10 },
{ header: '姓名', key: 'name', width: 32 },
{ header: '性别.', key: 'sex', width: 10, outlineLevel: 1 }
];
for (let i = 1; i <= 100000; i++) {
worksheet.addRow({index: i, name: '明明', sex: i % 2 > 0 ? '男' : '女'});
}
workbook.xlsx.writeBuffer().then(buffer => {
this.blobData = new Blob([buffer]);
this.export()
});
},
// 导出
export() {
saveAs(this.blobData, `${Date.now()}_feedback.xlsx`);
console.timeEnd('主线程导出')
console.timeEnd('worker导出')
},
},
mounted() {
this.worker = new Worker();
this.worker.onmessage = event => {
this.blobData = event.data;
this.export();
console.log('主线程收到回复,即将关闭worker连接');
this.worker.terminate();
}
this.timer = setInterval(() => {
this.timeStamp = Date.now();
})
},
beforeDestroy() {
this.worker?.terminate()
clearInterval(this.timer)
}
}
</script>
import Excel from "exceljs";
onmessage = function(e) {
console.log(e.data);
switch (e.data.command) {
case 'getBlob':
getBlob()
}
};
function getBlob() {
const workbook = new Excel.Workbook();
const worksheet = workbook.addWorksheet('My Sheet');
worksheet.columns = [
{ header: '序号', key: 'index', width: 10 },
{ header: '姓名', key: 'name', width: 32 },
{ header: '性别.', key: 'sex', width: 10, outlineLevel: 1 }
];
for (let i = 1; i <= 100000; i++) {
worksheet.addRow({index: i, name: '明明', sex: i % 2 > 0 ? '男' : '女'});
}
workbook.xlsx.writeBuffer().then(buffer => {
postMessage(new Blob([buffer]));
});
}
主线程导出: 13290.680908203125 ms
worker导出: 13147.514892578125 ms
压缩100张图片
<template>
<div class="hello">
{{ timeStamp }}
<button @click="compress">主线程压缩</button>
<button @click="useWorker">worker压缩</button>
<button @click="useWorkers">多线程worker压缩</button>
</div>
</template>
<script>
import Worker from './demo.worker.js';
export default {
name: 'CompressImage',
props: {
msg: String
},
data() {
return {
timeStamp: 0,
timer: null,
img: './1.png', // 6.92MB的图片
}
},
methods: {
// 单线程worker
async useWorker() {
console.time('worker压缩');
// Dom隔离,img元素传不过worker,用fetch读取图片,转成blob,传blob给worker
// Dom隔离,canvas元素传不不过worker
// worker没有document,不能创建传统的canvas,可以创建OffscreenCanvas。或者在主线程把传统canvas转成OffscreenCanvas,传给worker
// 传图像不传图片路径,避免相对路径的图片在worker中读取不到
const canvas = document.createElement('canvas');
const offscreenCanvas = canvas.transferControlToOffscreen();
let blobImgs = [];
for (let i = 0; i < 100; i++) {
blobImgs.push(await fetch(this.img).then(res => res.blob()));
}
this.worker.postMessage({
command: 'compress',
blobImgs,
canvas: offscreenCanvas
}, [offscreenCanvas])
},
// 多线程worker
async useWorkers() {
console.time(`多线程worker压缩`);
let promiseList = [];
for (let i = 1; i <= 5; i++) {
let blobImgs = [];
for (let i = 0; i < 20; i++) {
blobImgs.push(await fetch(this.img).then(res => res.blob()));
}
const promise = new Promise(resolve => {
const canvas = document.createElement('canvas');
const offscreenCanvas = canvas.transferControlToOffscreen();
const worker = new Worker();
worker.onmessage = event => {
resolve(event.data);
worker.terminate(); // 卸载worker
}
worker.postMessage({
command: 'compress',
blobImgs,
canvas: offscreenCanvas
}, [offscreenCanvas])
})
promiseList.push(promise);
}
await Promise.all(promiseList);
console.timeEnd(`多线程worker压缩`);
},
// 图片用Image请求
async compress() {
console.time('主线程压缩');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const img = new Image();
img.setAttribute('crossOrigin', 'Anonymous');
let imgs = [];
for (let i = 0; i < 100; i++) {
img.src = this.img;
const blobImg = await new Promise(resolve => {
img.onload = function() {
canvas.width = this.width;
canvas.height = this.height;
context.drawImage(img, 0, 0, this.width, this.height);
canvas.toBlob((blob) => {
resolve(blob)
}, 'image/jpeg', 0.75)
};
})
imgs.push(blobImg);
}
console.timeEnd('主线程压缩');
},
// 与worker线程一样,图片用fetch请求
async compress2() {
console.time('主线程压缩');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
let blobImgs = [];
for (let i = 0; i < 100; i++) {
blobImgs.push(await fetch(this.img).then(res => res.blob()));
}
await Promise.all(blobImgs.map(async blobImg => {
const img = await createImageBitmap(blobImg);
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0, img.width, img.height);
return new Promise(resolve => {
canvas.toBlob(blob => resolve(blob), 'image/jpeg', 0.75)
})
}))
console.timeEnd('主线程压缩');
},
},
mounted() {
this.worker = new Worker();
this.worker.onmessage = event => {
console.timeEnd('worker压缩')
// this.worker.terminate();
}
this.timer = setInterval(() => {
this.timeStamp = Date.now();
})
},
beforeDestroy() {
this.worker?.terminate()
clearInterval(this.timer)
}
}
</script>
onmessage = function(e) {
console.log(e.data);
switch (e.data.command) {
case 'compress':
compress(e.data);
break;
}
};
async function compress(data) {
const { blobImgs, canvas: offscreenCanvas } = data;
const context = offscreenCanvas.getContext('2d');
const compressedImgs = await Promise.all(blobImgs.map(async blobImg => {
const img = await createImageBitmap(blobImg);
offscreenCanvas.width = img.width;
offscreenCanvas.height = img.height;
context.drawImage(img, 0, 0, img.width, img.height);
// 没有toDataURL方法,用convertToBlob压缩,再用FileReader转成base64
return await offscreenCanvas.convertToBlob({ type: 'image/jpeg', quality: 0.75 });
}))
postMessage(compressedImgs);
}
每次压缩都刷新浏览器,避免浏览器缓存造成的误差
主线程压缩(Image): 46263.44287109375 ms
主线程压缩(fetch): 24482.360107421875 ms
worker压缩(fetch): 24908.89599609375 ms
多线程worker压缩(fetch): 19629.847900390625 ms (√)
补充
- Navigator.hardwareConcurrency可以看电脑的CPU是多少核的
- 6核CPU开10个worker也行,两者好像没什么联系
- worker会占据浏览器内存,可以在F12 – Memory中看到,用完了需要手动卸载,避免占用内存
-
不正确使用会造成浏览器内存不足(
SBOX_FATAL_MEMORY_EXCEEDED
)奔溃
一张blob图片:7267193字节
用postMessage传输给worker
1个worker,500图片,崩
5个worker,每个100图片,崩
5个worker,每个100图片,每个worker处理完后,卸载worker,崩
10个worker,每个50图片,每个worker处理完后,卸载worker,正常
10个worker,每个50图片,崩
-
postmessage
应该少量多次,用完的worker及时卸载,避免一直占用内存,浏览器内存不够就奔溃
版权声明:本文为weixin_44240581原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。