开篇引言
之前做过一个实时语音翻译系统的时候就用过webworker来提高语音翻译识别的页面性能,最近从梁公子的图片渲染相关代码中再一次见到webWorker的身影,不免还是有点熟悉的!
这次业务场景是使用canvas在图片上的标注一些特征区域和特征点,但是由于一次性页面中绘制大量的高清图片(下图只是截取了页面局部,整个页面的图片是非常多的)会导致浏览器页面卡死,而采用webWorker结合OffscreenCanvas离屏渲染可以最大性能的利用客户端的线程,把绘制放到web worker中绘制的过程不阻塞主线程的运行,提升渲染性能。
所以说在保持一定专注领域范围内,持久的学习总有一天都是会有用的!在此特别鸣谢梁公子,也祝他前程似锦!
OffscreenCanvas 浏览器离屏渲染API
OffscreenCanvas提供了一个可以脱离屏幕渲染的canvas对象。它在窗口环境和web worker环境均有效,主要用于提升 Canvas 2D/3D 绘图的渲染性能和使用体验;
OffscreenCanvas和canvas都是渲染图形的对象。 不同的是canvas只能在window环境下使用,而OffscreenCanvas即可以在window环境下使用,也可以在web worker中使用,这让不影响浏览器主线程的离屏渲染成为可能。
具体方法参考MDN developer.mozilla.org/zh-CN/docs/…
与之关联的还有ImageBitmap对象和ImageBitmapRenderingContext。
ImageBitmap
ImageBitmap对象表示能够被绘制到 canvas上的位图图像,具有低延迟的特性。 ImageBitmap提供了一种异步且高资源利用率的方式来为WebGL的渲染准备基础结构。
transferToImageBitmap函数
通过transferToImageBitmap函数可以从OffscreenCanvas对象的绘制内容创建一个ImageBitmap对象。该对象可以用于到其他canvas的绘制。
比如本文就是,把一个比较耗费时间的绘制放到web worker下的OffscreenCanvas对象上进行,绘制完成后,创建一个ImageBitmap对象,并把该对象传递给页面端,在页面端绘制ImageBitmap对象。
draw_workers.js
一个进程的worker要处理的任务代码如下:
let ctx = self;
ctx.addEventListener('message', ({ data }) => {
console.log('OffscreenCanvas.data', data);
let canvas = new OffscreenCanvas(data.oriSize.w, data.oriSize.h); // 浏览器离屏渲染API(传入参数为宽高)
let context = canvas.getContext('2d'); // 为offscreencanvas对象返回一个渲染画布
ctx.createImageBitmap(data.blob).then(imageBitmap => {
// data.blob 为图片转换成的blob对象
context.drawImage(imageBitmap, 0, 0);
const px = data.oriSize.w;
for (let item of data.point) {
context.fillStyle = '#ffbc00'; //'#ffbc00'
context.beginPath(); //标志开始一个路径
context.arc(item.x, item.y, px / 480, 0, 2 * Math.PI, true); //在canvas中绘制圆点
context.fill();
context.strokeStyle = '#ffbc00';
context.stroke();
}
for (let item of data.rect) {
context.lineWidth = px / 240;
context.strokeStyle = item.color;
context.strokeRect(item.x, item.y, item.width, item.height); //在canvas中绘制矩形框
context.font = parseInt(px / 19.2) + 'px Verdana';
context.fillStyle = item.color;
context.fillText(item.label || '', item.x, item.y);
}
// const t1 = Date.now()
const imageBitmap2 = canvas.transferToImageBitmap(); // 绘制完成后,创建一个ImageBitmap对象,并把该对象传递给页面端,在页面端绘制ImageBitmap对象。
postMessage({
imageBitmap: imageBitmap2
});
// canvas.convertToBlob({
// type: 'image/webp'
// }).then((blob) => {
// console.log(Date.now() - t1)
// const reader = new FileReader();
// reader.readAsDataURL(blob);
// return new Promise(resolve => {
// reader.onloadend = () => {
// resolve(reader.result);
// };
// });
// }).then((base64) => {
// // 把取到的base64 传给主线程
// ctx.postMessage(base64)
// })
});
});
// var offscreen, ctx;
// onmessage = function () {
// init();
// draw();
// }
// function init() {
// offscreen = new OffscreenCanvas(512, 512);
// ctx = offscreen.getContext("2d");
// }
// function draw() {
// ctx.clearRect(0, 0, offscreen.width, offscreen.height);
// for (var i = 0; i < 10000; i++) {
// for (var j = 0; j < 1000; j++) {
// ctx.fillRect(i * 3, j * 3, 2, 2);
// }
// }
// var imageBitmap = offscreen.transferToImageBitmap();
// postMessage({
// imageBitmap: imageBitmap
// }, [imageBitmap]);
// }
利用webWork线程池,最大化利用客户机渲染性能
一般电脑都是4核8线程,故此处创建最多8线程的线程池,用于多个图像并行的canvas绘制
// worker线程池
const pool = [];
// 默认8个常驻线程
for (let index = 0; index < 8; index++) {
const worker = new Worker('draw_workers.js'); // 此处注意引入路径
pool.push({
workerId: index,
worker,
status: 'free' // free | busy
});
}
// 获取当前闲置(free)的线程,如果都在busy,则等到100ms再试一次
async function findFreePool() {
while (true) {
const poolItem = pool.find(item => item.status === 'free'); // 找到一个可用线程
if (poolItem) {
return poolItem;
} else {
await timeOut(100);
}
}
}
function timeOut(s) {
return new Promise(r => setTimeout(r, s));
}
export function work(data) {
return new Promise(async (resolve, reject) => {
const poolItem = await findFreePool();
poolItem.status = 'busy'; // 获取到free的线程就让他busy起来,去处理事件
poolItem.worker.onmessage = e => {
resolve(e);
poolItem.status = 'free'; // 收到工作完成的消息之后就释放该进程
};
poolItem.worker.postMessage(data); // 将data内容传递给draw_workers的worker中
});
}
vue页面使用
<template>
<div>
<canvas :ref="canvasDom" :width="width" :height="height" style="max-width:100%;max-height:100%;"></canvas>
</div>
</template>
<script>
import {
work
} from './workerPool'
export default {
name: 'drawRect',
props: {
// 图像路径
url: {
type: String
},
// 矩形框坐标数组
rect: {
type: Array
},
// 点数组
point: {
type: Array
}
},
data() {
return {
itemUrl: null,
canvasBitmap: null,
ctxBitmap: null,
width: 0,
height: 0,
canvasDom: Date.now() + '_' + Math.random(),
worker: null,
};
},
watch: {
url() {
this.draw();
},
rect() {
this.draw();
},
point() {
this.draw();
}
},
created() {},
async mounted() {
this.draw();
},
methods: {
async draw() {
if (!this.url || !this.rect) {
console.warn('画框组件缺少参数');
return;
}
const blob = await this.loadImageAsync(this.url);
var nImg = new Image();
nImg.onload = () => {
// onload之后获取到图像属性
const w = nImg.width;
const h = nImg.height;
this.width = w
this.height = h
this.$nextTick(async () => {
this.canvasBitmap = this.$refs[this.canvasDom];
this.ctxBitmap = this.canvasBitmap.getContext('2d'); //
// 拿到新的worker 并将数据传给worker,之后workerPool 通过postmessage将数据传递给draw_workers中绘制canvas对象
const e = await work({
blob,
oriSize: {
w,
h
},
rect: this.rect || [],
point: this.point || []
})
this.$emit('getImageData', e.data.imageBitmap) // 将二进制图像向父组件抛出
this.ctxBitmap.drawImage(e.data.imageBitmap, 0, 0);
this.ctxBitmap.restore(); // 保存canvas结果
})
};
nImg.src = URL.createObjectURL(blob);
},
loadImageAsync(imageUrl) {
return new Promise(resolve => {
fetch(imageUrl).then(response => {
// fetch 不仅可以将请求结果转为json,也可以直接通过response.blob()的方式直接转为blob对象
resolve(response.blob());
});
});
}
}
};
</script>
最近开通了订阅号——“前端之帆”,希望小伙伴们赏个脸,来评论骚扰哈!二维码奉上
相关连接
-
MDN 使用 Web Workers developer.mozilla.org/zh-CN/docs/…
-
OffscreenCanvas-离屏canvas使用说明 cloud.tencent.com/developer/a…
-
Canvas基础demo 之 用Canvas创造一个太阳系 juejin.cn/post/684490…