webWorker在canvas离屏渲染中的应用

4,752 阅读3分钟

开篇引言

之前做过一个实时语音翻译系统的时候就用过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>

最近开通了订阅号——“前端之帆”,希望小伙伴们赏个脸,来评论骚扰哈!二维码奉上

FESail

相关连接

  1. MDN 使用 Web Workers developer.mozilla.org/zh-CN/docs/…

  2. OffscreenCanvas-离屏canvas使用说明 cloud.tencent.com/developer/a…

  3. Canvas基础demo 之 用Canvas创造一个太阳系 juejin.cn/post/684490…