浏览器多线程离屏渲染压缩打包方案
浏览器多线程离屏渲染压缩打包方案
最近朋友跟我交流了一个场景,他有需求要用浏览器实时生成上万个二维码并打包压缩。现在功能是实现了,就是耗时长,而且一旦开始生成之后,页面卡顿的很厉害。
我一听应该是 大量的 渲染
, 转化
,压缩
这类的计算阻塞了 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
,在 qrcode
的 errorLevel
为 low
, 不进行额外压缩的情况下。
每次生成图片大约 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) } }) 复制代码
编写完成后,然后再使用webpack
的 worker-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 离屏渲染
Web Worker 全局作用域中不存在 Image
有绘制过的 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
对象的形式进行图像传递。
为什么?
首先 ImageBitmap
是 Transferable
(见上部分Worker多线程压缩:引用转移介绍)
同时创建 ImageBitmap
的 API: createImageBitmap
方法同时存在 windows
和 workers
中. 它接受各种不同的图像来源。例如它可以处理我们最常使用的 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
对象。
主线程只负责创建 ImageBitmap
和 ArrayBuffer
这类的 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) 复制代码
优先去除兼容性最差的
OffscreenCanvas
次级去除
Web Worker
, 只使用Js主线程
方案示例见 源代码
方案总结
优点:
处理大体积图片的性能更优
不阻塞主js线程
缺点:
兼容性差, 需要降级方案
实现相比单线程版本较为复杂,心智负担较重,容易出bug
处理小图片可能不如主线程
不同浏览器性能差异较大,难以统一用户体验
作者:icebreaker
链接:https://juejin.cn/post/7039335445854945287