webWorker在canvas離屏渲染中的應(yīng)用
https://juejin.cn/post/6955013319702872095
前做過一個(gè)實(shí)時(shí)語音翻譯系統(tǒng)的時(shí)候就用過webworker來提高語音翻譯識(shí)別的頁面性能,最近從梁公子的圖片渲染相關(guān)代碼中再一次見到webWorker的身影,不免還是有點(diǎn)熟悉的!
這次業(yè)務(wù)場(chǎng)景是使用canvas在圖片上的標(biāo)注一些特征區(qū)域和特征點(diǎn),但是由于一次性頁面中繪制大量的高清圖片(下圖只是截取了頁面局部,整個(gè)頁面的圖片是非常多的)會(huì)導(dǎo)致瀏覽器頁面卡死,而采用webWorker結(jié)合OffscreenCanvas離屏渲染可以最大性能的利用客戶端的線程,把繪制放到web worker中繪制的過程不阻塞主線程的運(yùn)行,提升渲染性能。
所以說在保持一定專注領(lǐng)域范圍內(nèi),持久的學(xué)習(xí)總有一天都是會(huì)有用的!在此特別鳴謝梁公子,也祝他前程似錦!
OffscreenCanvas 瀏覽器離屏渲染API
OffscreenCanvas提供了一個(gè)可以脫離屏幕渲染的canvas對(duì)象。它在窗口環(huán)境和web worker環(huán)境均有效,主要用于提升 Canvas 2D/3D 繪圖的渲染性能和使用體驗(yàn);
OffscreenCanvas和canvas都是渲染圖形的對(duì)象。 不同的是canvas只能在window環(huán)境下使用,而OffscreenCanvas即可以在window環(huán)境下使用,也可以在web worker中使用,這讓不影響瀏覽器主線程的離屏渲染成為可能。
具體方法參考MDN developer.mozilla.org/zh-CN/docs/…
與之關(guān)聯(lián)的還有ImageBitmap對(duì)象和ImageBitmapRenderingContext。
ImageBitmap
ImageBitmap對(duì)象表示能夠被繪制到 canvas上的位圖圖像,具有低延遲的特性。 ImageBitmap提供了一種異步且高資源利用率的方式來為WebGL的渲染準(zhǔn)備基礎(chǔ)結(jié)構(gòu)。
transferToImageBitmap函數(shù)
通過transferToImageBitmap函數(shù)可以從OffscreenCanvas對(duì)象的繪制內(nèi)容創(chuàng)建一個(gè)ImageBitmap對(duì)象。該對(duì)象可以用于到其他canvas的繪制。
比如本文就是,把一個(gè)比較耗費(fèi)時(shí)間的繪制放到web worker下的OffscreenCanvas對(duì)象上進(jìn)行,繪制完成后,創(chuàng)建一個(gè)ImageBitmap對(duì)象,并把該對(duì)象傳遞給頁面端,在頁面端繪制ImageBitmap對(duì)象。
draw_workers.js
一個(gè)進(jìn)程的worker要處理的任務(wù)代碼如下:
let ctx = self;
ctx.addEventListener('message', ({ data }) => {
console.log('OffscreenCanvas.data', data);
let canvas = new OffscreenCanvas(data.oriSize.w, data.oriSize.h); // 瀏覽器離屏渲染API(傳入?yún)?shù)為寬高)
let context = canvas.getContext('2d'); // 為offscreencanvas對(duì)象返回一個(gè)渲染畫布
ctx.createImageBitmap(data.blob).then(imageBitmap => {
// data.blob 為圖片轉(zhuǎn)換成的blob對(duì)象
context.drawImage(imageBitmap, 0, 0);
const px = data.oriSize.w;
for (let item of data.point) {
context.fillStyle = '#ffbc00'; //'#ffbc00'
context.beginPath(); //標(biāo)志開始一個(gè)路徑
context.arc(item.x, item.y, px / 480, 0, 2 * Math.PI, true); //在canvas中繪制圓點(diǎn)
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(); // 繪制完成后,創(chuàng)建一個(gè)ImageBitmap對(duì)象,并把該對(duì)象傳遞給頁面端,在頁面端繪制ImageBitmap對(duì)象。
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線程池,最大化利用客戶機(jī)渲染性能
一般電腦都是4核8線程,故此處創(chuàng)建最多8線程的線程池,用于多個(gè)圖像并行的canvas繪制
// worker線程池
const pool = [];
// 默認(rèn)8個(gè)常駐線程
for (let index = 0; index < 8; index++) {
const worker = new Worker('draw_workers.js'); // 此處注意引入路徑
pool.push({
workerId: index,
worker,
status: 'free' // free | busy
});
}
// 獲取當(dāng)前閑置(free)的線程,如果都在busy,則等到100ms再試一次
async function findFreePool() {
while (true) {
const poolItem = pool.find(item => item.status === 'free'); // 找到一個(gè)可用線程
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'; // 收到工作完成的消息之后就釋放該進(jìn)程
};
poolItem.worker.postMessage(data); // 將data內(nèi)容傳遞給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
},
// 矩形框坐標(biāo)數(shù)組
rect: {
type: Array
},
// 點(diǎn)數(shù)組
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('畫框組件缺少參數(shù)');
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 并將數(shù)據(jù)傳給worker,之后workerPool 通過postmessage將數(shù)據(jù)傳遞給draw_workers中繪制canvas對(duì)象
const e = await work({
blob,
oriSize: {
w,
h
},
rect: this.rect || [],
point: this.point || []
})
this.$emit('getImageData', e.data.imageBitmap) // 將二進(jìn)制圖像向父組件拋出
this.ctxBitmap.drawImage(e.data.imageBitmap, 0, 0);
this.ctxBitmap.restore(); // 保存canvas結(jié)果
})
