webworker批量压缩图片踩坑

294 阅读2分钟

记录下最近遇到的需求:加载三四百张图片,压缩后再展示给用户。最终方案是用worker多线程进行压缩,这里记录下踩过的坑。

大概最终实现效果:未压缩体积297MB,压缩后体积21MB。

压缩图片方法

// vue
toCompressedPic(url) {
      const img = new Image()
      // 允许图片资源跨域
      img.crossOrigin = 'Anonymous'
      img.src = url
      img.onload = () => {
        // 绘制在canvas上
        const canvas = new OffscreenCanvas(img.width, img.height)
        const ctx = canvas.getContext('2d')
        ctx.drawImage(img, 0, 0)
​
        let quality = 0.8
        const maxSize = 100 * 1024
        const cb = (blob) => {
          quality -= 0.1
          if (blob.size > maxSize && quality > 0.1) {
            canvas.convertToBlob(cb, 'image/jpeg', quality)
          } else {
            // 输出结果
            this.src = URL.createObjectURL(blob)
          }
        }
        canvas.convertToBlob(cb, 'image/jpeg', quality)
      }
    }

组件内部压缩

组件加载完图片调用方法压缩,用时约52S,但页面会有略微卡顿。

Vuex单线程压缩

为了方便统一处理,存储在vuex数组中,轮询进行压缩,压缩完成就调用回调函数将数据返回给原组件。

   // vue
   this.$store.dispatch('toCompressedPic', {
      url: this.url,
      cb: (url) => {
        this.src = url
      }
    })

用时约74S,无卡顿。

多worker线程池压缩图片:

还是统一存储在Vuex中。但初次使用worker,遇到了一些问题:

worker请求JS文件存在跨域

解决办法:本地Node提供静态资源并设置请求头允许跨域。

// node托管静态文件
var express = require('express');
var fs = require('fs');
var app = express();
​
app.use('/public',function(req,res) {
  fs.readFile('./public/'+req.url , 'utf-8' , function(err,data) {
    if (err) {
      res.send(err)
    }else{
      res.setHeader('Content-Type', 'text/plain');
      res.setHeader('Access-Control-Allow-Origin', '*');
      res.send(data)    
    }
  })
})
​
app.listen(3000,function () {
  console.log("启动成功, http://localhost/3000")
})

worker线程无法使用Document

解决办法:OffscreenCanvas 和 fetch请求图片资源。

图片资源存在跨域问题,无法在主线程和worker线程间传递

解决办法:worker线程自己调用fetch请求图片。

// worker
self.onmessage = async (e) => {
  const response = await fetch(e.data);
  const blob = await response.blob();
  createImageBitmap(blob).then(imageBitmap => {
    const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height)
    let ctx = canvas.getContext('2d');
    ctx.drawImage(imageBitmap, 0, 0)
​
    let quality = 0.8
    const maxSize = 100 * 1024
    const cb = (blob) => {
      quality -= 0.1
      if (blob.size > maxSize && quality > 0.1) {
        canvas.convertToBlob({ type: 'image/jpeg', quality: quality }).then(cb)
      } else {
        self.postMessage(URL.createObjectURL(blob))
      }
    }
    canvas.convertToBlob({ type: 'image/jpeg', quality: quality }).then(cb)
  })
}

开启一个8核的线程池

fetch(//远程地址)
  .then(response => response.text())
  .then(scriptText => {
    const blob = new Blob([scriptText], { type: 'application/javascript' })
    const blobUrl = URL.createObjectURL(blob)
    for (let index = 0; index < 8; index++) {
      const worker = new Worker(blobUrl)
      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))
}

最终效果:用时约26S,无卡顿。

参考文章:juejin.cn/post/695501…