Gif 截取首帧
商家上传 Gif 作为商品图,过程中转为 WebP 格式已优化了约 60% 的体积,但仍接近 2M 的图片对于网站加载过程中用户体验无疑是一场灾难。因此从商家侧上传 Gif 时进行截取首帧作为动图的封面图,在网站加载中预显示,优化用户体验。
Gif 解帧网上也有多种方案,若限定于浏览器端环境的话,可选的其实也就不多。
业界一些类似于 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
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)
与上一种方式对比,避免了 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
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
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
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
document.getElementById('img').src = URL.createObjectURL(blob)
多次测试平均整体耗时 50+ms,
image.onload
解析 Blob 仅花费了 10+ms,这部分方案梳理发布到 NPM 库 —
Gifff
。另外这种方式确实能成功导出一帧,但是否首帧?这个问题就留给各位思考了🤔
Playground
WebWorker + OffscreenCanvas + ImageBitmap
尝试一下 WebWorker 生态环境,例如
_
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 能被支持,是个不错的开始。