阅读 251

浏览器多线程离屏渲染压缩打包方案

浏览器多线程离屏渲染压缩打包方案

最近朋友跟我交流了一个场景,他有需求要用浏览器实时生成上万个二维码并打包压缩。现在功能是实现了,就是耗时长,而且一旦开始生成之后,页面卡顿的很厉害。

我一听应该是 大量的 渲染, 转化,压缩 这类的计算阻塞了 Js执行主线程 导致的,于是开始尝试对方案进行优化。

首先先复现Js主线程方案

这个方案心智负担最低,无非是 Canvas 渲染转化为 blob/(or others), jszip 添加 blob 并进行压缩, 最后下载保存,执行的代码摘要如下:

import JSZip from 'jszip' async function download(){     // canvas do sth     const zip = new JSZip()     await new Promise((resolve)=>{         canvas.toBlob((blob) => {             zip.file(filename, blob!)             resolve(blob)         })     })     const content = await zip.generateAsync(         {           type: 'blob'         }     )     saveAs(content, zipName) } 复制代码

实现是非常简单的。笔者也复现了生成 10000 个二维码的 case,在 qrcodeerrorLevellow , 不进行额外压缩的情况下。

每次生成图片大约 3-4kb(取决于携带参数的大小),生成时间约为 190,257.10ms ,压缩时间为 13,531ms, 总耗时 203,788.10ms

后来经过反复测试,得出下列几个影响因素:

  • 压缩等级越高,压缩越慢

  • 生成图片体积越大,生成速度越慢,压缩速度也越慢

另外像这类的高耗时的工作任务,一定要添加 onProgress 这样一个 hook,方便用户自定义进度条来优化体验,同时也要防止用户误操作造成功亏一篑。

Worker多线程压缩

既然现在主线程被阻塞了,我们自然而然就想到了 Web Worker, 于是笔者使用它来进行压缩图片的工作。

在挑选测试素材时,使用了一张 16MB 的图片,尝试下来,压缩时间显著高于图片的生成时间。(体积较小图片其实是没有必要的,主线程本身压缩速度足够快)

worker 代码摘要如下:

// main.worker.ts import JSZip from 'jszip' const worker: Worker = self as any async function doZip (arraybuffer: ArrayBuffer) {   const zip = new JSZip()   const filename = 'test.png'   const blob = new Blob([arraybuffer])   zip.file(filename, blob)   const content = await zip.generateAsync(     {       type: 'arraybuffer'     },     ({ percent }) => {       // 压缩进度条       const event: MainWorkerEventData = {         type: 'percent',         percent       }       worker.postMessage(event)     }   )   const finish: MainWorkerEventData = {     type: 'save',     content: content   }   worker.postMessage(finish, [content]) } worker.addEventListener('message', (event:MessageEvent<ZipWorkerRequestEventData>) => {   const data = event.data   if (data.type === 'zip') {     doZip(data.arraybuffer)   } }) 复制代码

编写完成后,然后再使用webpackworker-loader 加载进来使用:

import MainWorker from 'worker-loader!@/workers/main.worker'

此时页面代码摘要为:

// vue3 ts canvas.toBlob((blob)=>{   blob.arrayBuffer().then((ab)=>{     const message: ZipWorkerRequestEventData = {       type: 'zip',       arraybuffer: ab     }     worker.postMessage(message, [ab])   }) }) worker.onmessage = (event: MessageEvent<ZipWorkerResponseEventData>) => {   const data = event.data   if (data.type === 'save') {     const blob = new Blob([data.content as ArrayBuffer])     saveAs(blob, 'test.zip')     // 下载成功!   } else if (data.type === 'percent') {     // 进度条     percent.value = data.percent ?? 0   } } 复制代码

有点要特别注意: postMessage 推荐使用对象转移的写法。这里要解释一下,主线程与 Web Workers 之间的通信,并不是对象引用的传递,而是序列化/反序列化的过程,当对象非常庞大时,序列化和反序列化都会消耗大量计算资源,降低运行速度。对象转移就是将对象引用零成本转交给 Web Workers 的上下文,而不需要进行结构拷贝。

需要注意的是,对象引用转移后,原先上下文就无法访问此对象了,需要在 Web Workers 再次将对象还原到主线程上下文后,主线程才能正常访问被转交的对象。

其中可以进行引用转移的对象,只有:

  • ArrayBuffer

  • MessagePort

  • ReadableStream

  • WritableStream

  • TransformStream

  • AudioData

  • ImageBitmap

  • VideoFrame

  • OffscreenCanvas

详见 MDN文档

这也是我们把图片转化为 ArrayBuffer 对象进行传输的主要原因。不然复制一份数据,既浪费内存又浪费算力。

通过这种方案,就把压缩这一部分的计算转移到了 Web Worker 那里去,从而避免了JS主线程的阻塞。但是这适用场景不是那么多,因为这对于批量小图片的压缩打包,收益不是很大,另外生成图片也非常耗时耗资源,这一部分并没有解决,所以接下来我们尝试把 Canvas 图片渲染生成 也放入 Web Worker 那里去进行做。

OffscreenCanvas Worker 离屏渲染

  1. Web Worker 全局作用域中不存在 Image

  2. 有绘制过的 Canvas 无法转化为  OffscreenCanvas, 会报出:

Failed to execute 'transferControlToOffscreen' on 'HTMLCanvasElement': Cannot transfer control from a canvas that has a rendering context. 错误

方案

首先添加 @types 以引入智能提示: yarn add -D @types/offscreencanvas

另外确立js主线程和 web worker 之间的传输以 ImageBitmap 对象的形式进行图像传递。

为什么?

首先 ImageBitmapTransferable(见上部分Worker多线程压缩:引用转移介绍)

同时创建 ImageBitmap 的 API: createImageBitmap 方法同时存在 windowsworkers 中. 它接受各种不同的图像来源。例如它可以处理我们最常使用的 HTMLImageElement 对象。

这里我以 ImageBitmap 为例构建 Web Worker 部分代码:

// main.work.ts async function getCanvasBlob(bitmap: ImageBitmap): Promise<Blob> {     const canvas = new OffscreenCanvas(bitmap.width, bitmap.height)     const ctx = canvas.getContext('2d') as OffscreenCanvasRenderingContext2D     ctx.drawImage(bitmap, 0, 0)     // do sth     return await canvas.convertToBlob() } 复制代码

这部分代码目的在于取代主线程中创建的 HTMLCanvasElement 对象,更换为 Web Worker 中创建的 OffscreenCanvas 对象。

主线程只负责创建 ImageBitmapArrayBuffer 这类的 Transferable_objects 来和 Worker 进行数据通信。

并最终 Worker 输出一个 ArrayBuffer 交给主线程组装二进制对象并进行浏览器下载行为。

优雅的兼容性降级方案

方案主要存在 2 个可能不兼容的点:

OffscreenCanvas

OffscreenCanvas 除了 chormuim 内核的那一批浏览器(chrome.edge)支持较好之外,像 FF,Safari,ie 均不支持 (意外的是 opera居然支持)
附Browser compatibility

Web Worker

Web Worker 大部分都支持(包括 ie10+) , 不过各个浏览器支持的特性存在一些细小的差异。
附Browser compatibility

兼容标志位

// 是否支持 web worker export const isSupportWorker = Boolean(window.Worker) // 是否支持 OffscreenCanvas export const isSupportOffscreenCanvas = Boolean(window.OffscreenCanvas) 复制代码

  1. 优先去除兼容性最差的OffscreenCanvas

  2. 次级去除 Web Worker , 只使用Js主线程

方案示例见 源代码

方案总结

优点:

  1. 处理大体积图片的性能更优

  2. 不阻塞主js线程

缺点:

  1. 兼容性差, 需要降级方案

  2. 实现相比单线程版本较为复杂,心智负担较重,容易出bug

  3. 处理小图片可能不如主线程

  4. 不同浏览器性能差异较大,难以统一用户体验


作者:icebreaker
链接:https://juejin.cn/post/7039335445854945287

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐