添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Gif 截取首帧

商家上传 Gif 作为商品图,过程中转为 WebP 格式已优化了约 60% 的体积,但仍接近 2M 的图片对于网站加载过程中用户体验无疑是一场灾难。因此从商家侧上传 Gif 时进行截取首帧作为动图的封面图,在网站加载中预显示,优化用户体验。

Gif 解帧网上也有多种方案,若限定于浏览器端环境的话,可选的其实也就不多。

GIFrame

业界一些类似于 GIFrame 的库可在浏览器端进行解帧,同时无需解析整个 Gif 图片数据,可快速拿到首帧数据(100+ms)。但解析器工作主要运行在 JS 主线程,JS Heap 约 20+m,挺耗性能的任务。

HTMLImageElement + HTMLCanvasElement

一种相对直接简单粗暴而兼容性可靠的方式,例如


_ 15
const reader = new FileReader()
_ 15
reader.readAsDataURL(event.target.files[0])
_ 15
await new Promise((resolve) => (reader.onload = resolve))
_ 15
const image = new Image()
_ 15
await new Promise(resolve => {
_ 15
image.src = reader.result
_ 15
image.onload = resolve
_ 15
})
_ 15
const canvas = document.getElementById('canvas')
_ 15
const ctx = canvas.getContext('2d')
_ 15
canvas.width = image.width
_ 15
canvas.height = image.height
_ 15
ctx.drawImage(image, 0, 0, image.width, image.height)
_ 15
const base64 = canvas.toDataURL('image/png', 1.0)
_ 15
console.log(base64)

与上一种方式对比,避免了 Gif JS 解析器在主线程的性能消耗,但测试对比发现拿到帧的数据的明显会慢些(800+ms),怀疑 toDataURL 同步转换格式、转换字符串等逻辑影响,所以改为


_ 17
const reader = new FileReader()
_ 17
reader.readAsDataURL(event.target.files[0])
_ 17
await new Promise((resolve) => (reader.onload = resolve))
_ 17
const image = new Image()
_ 17
await new Promise(resolve => {
_ 17
image.src = reader.result
_ 17
image.onload = resolve
_ 17
})
_ 17
const canvas = document.getElementById('canvas')
_ 17
const ctx = canvas.getContext('2d')
_ 17
canvas.width = image.width
_ 17
canvas.height = image.height
_ 17
ctx.drawImage(image, 0, 0, image.width, image.height)
_ 17
const blob = await new Promise((resolve) => canvas.toBlob((blob) => {
_ 17
resolve(blob);
_ 17
}))
_ 17
document.getElementById('img').src = URL.createObjectURL(blob)

消耗的时间仍差别不大,后续调试发现最长的耗时在于 image.onload 解析 base64 部分(700+ms),再改为


_ 17
const reader = new FileReader()
_ 17
reader.readAsArrayBuffer(event.target.files[0])
_ 17
await new Promise((resolve) => (reader.onload = resolve))
_ 17
const image = new Image()
_ 17
await new Promise(resolve => {
_ 17
image.src = URL.createObjectURL(new Blob([reader.result]));
_ 17
image.onload = resolve
_ 17
})
_ 17
const canvas = document.getElementById('canvas')
_ 17
const ctx = canvas.getContext('2d')
_ 17
canvas.width = image.width
_ 17
canvas.height = image.height
_ 17
ctx.drawImage(image, 0, 0, image.width, image.height)
_ 17
const blob = await new Promise((resolve) => canvas.toBlob((blob) => {
_ 17
resolve(blob);
_ 17
}))
_ 17
document.getElementById('img').src = URL.createObjectURL(blob)

多次测试平均整体耗时 50+ms, image.onload 解析 Blob 仅花费了 10+ms,这部分方案梳理发布到 NPM 库 — Gifff 。另外这种方式确实能成功导出一帧,但是否首帧?这个问题就留给各位思考了🤔

Playground

WebWorker + OffscreenCanvas + ImageBitmap

尝试一下 WebWorker 生态环境,例如


_ 10
// worker.js
_ 10
const imageBitmap = await createImageBitmap(blob)
_ 10
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height)
_ 10
const ctx = canvas.getContext('2d')
_ 10
ctx.drawImage(imageBitmap, 0, 0, imageBitmap.width, imageBitmap.height)
_ 10
const blob = await canvas.convertToBlob({ type: 'image/png' })
_ 10
self.postMessage(blob)

但查了一下 caniuse,兼容性的问题算是无解了。

WebAssembly + Rust

先查了 caniuse,主流 PC 端 Chrome、Safari 版本 WebAssembly 能被支持,是个不错的开始。