需求
移动端页面,原本是一个大背景图,现在有一部分需要做成动画,而这个动画大概比较复杂,所以由设计来做成 GIF。于是重新切图,将那部分不再保留在背景图上,用 GIF 来展示。但问题是 GIF 太大,于是产品问能不能在弱网时先展示静态图。
方法
vue img @load v-if
于是我就在 GIF 完成加载前展示 jpg 图片,因为是在 Vue 中,所有直接使用 img 的 load 事件就可以了,但是没起作用:
<img
v-if="isLoad"
src="path-to-your-gif.gif"
alt="动图"
@load="isLoad = true"
/>
<img
v-else
src="path-to-your-jpg.jpg"
alt="静态图片"
/>
new Image()
利用 new Image() 创建一个新的 image 对象,并开始加载 GIF,当加载完成(onload 事件触发)时,将展示的图片 src 更新为 GIF 的 url,重新渲染 img 元素。
<img
:src="imgSrc"
alt=""
/>
const imgSrc = ref("");
const img = new Image();
img.src = "path-to-your-jpg.jpg";
img.onload = () => {
imgSrc.value = "path-to-your-gif.gif";
};
Vue img @load v-show
后来才发现,第一种方式不生效的原因是使用了 v-if,当使用 v-if 时,页面渲染就没有 v-else,所以 GIF 加载完成时,也无法展示。因为我一开始背景图上还有图片,所以没有发现。
<img
v-show="isLoad"
src="path-to-your-gif.gif"
alt="动图"
@load="isLoad = true"
/>
<img
v-show="!isLoad"
src="path-to-your-jpg.jpg"
alt="静态图片"
/>
background
利用背景图,也可以做占位图。但是它的问题是,在加载的过程中,图片从上到下逐渐的覆盖背景图,有点奇怪。不过用背景图也有好处,那就是在图片加载失败时,仍然能展示静态图。
.img {
background-image: url("path-to-your-jpg.jpg");
background-size: 100%;
background-position: center;
background-repeat: no-repeat;
}
GIF
后来发现,其实 GIF 图是逐帧加载的,所以默认一开始就会显示静态图,并不需要再用一张静态图占位。我搜了一下,可能大部分浏览器是支持这样的。
也就是说,完全不做任何处理。
<img
src="path-to-your-gif.gif"
alt="动图"
/>
但是这种方式有它的缺点,和以上占位图的方式比较:
- 占位图:在完全加载完成之前,显示 jpg 图片,但是用户可能不知道这是个动图。而且 jpg 图片和 GIF 替换时会有一闪的感觉。
- 直接 GIF:逐帧加载,在完全加载完成之前,会显示一个比较卡顿的过程。
考虑到如果大多数情况下,用户网速 OK,那似乎直接 GIF 会更好一点。
第三方库
简单搜了下,vue-load-image 短小精悍、方便易用。它的功能很简单,就是在图片加载完成之前显示 loader,加载失败显示失败信息。也大概看了下源码,其本质也是使用 new Image 加载图片,onload 事件、onerror 事件时分别改变状态 status:
<template>
<div class="vue-load-image">
<slot v-if="status === 'loaded'" name="image" />
<slot v-else-if="status === 'failed'" name="error" />
<slot v-else-if="status === 'loading'" name="preloader" />
</div>
</template>
createLoader() {
this.destroyLoader()
this.img = new Image()
this.img.onload = this.handleLoad
this.img.onerror = this.handleError
this.img.crossOrigin = this.crossOrigin
this.img.src = this.src
},
handleLoad() {
this.destroyLoader()
this.status = Status.LOADED
this.$emit('onLoad')
},
handleError(error) {
this.destroyLoader()
this.status = Status.FAILED
this.$emit('onError', error)
},
还有更多第三方库,比如:vue-lazyload,功能更多一些。
总结
前端就是关注用户体验,前端就是关注各种细节,实际工作中,每一个小小的问题,如果深入探索,就能发现更多本质的东西。对于初学者来说,不要放过任何一个你不能理解的问题,对于职场人来说,被迫去做去思考你不感兴趣、你觉得不重要的事情,也许很快你就会有新的收获。
不过对于 GIF 图为什么那么大,我还是不理解,也没有压缩的空间了,这个也需要深入的学习制作一下。
还有是不是并不需要 GIF,用 CSS 可以实现呢?用 CSS 或 js 实现那样的动画需要多大的成本,性能又如何呢?
CSS
更新:2023年12月6日
最后,还真试了一下 css 动画,因为 GIF 时长较长,所以帧数多图片多,几十张图片:
.img {
background-image: url("path-to-your-jpg.jpg");
background-size: 100%;
background-position: center;
background-repeat: no-repeat;
animation: playGif 7s steps(84) infinite;
}
@keyframes playGif {
0% { background-image: url('path-to-your-jpg.jpg'); }
1.19% { background-image: url('path-to-your-jpg.jpg'); }
...
100% { background-image: url('path-to-your-jpg.jpg'); }
}
图片压缩之后并不比 GIF 小,而网络请求实在惊人。
AVIF
再次更新:2023年12月7日13:43:52
尝试
昨晚正好在 Twitter 上发现有人用 AVIF 替换 GIF,大大节省了 CDN。于是我也尝试了一下。昨晚就直接用 GIF 转了 AVIF 格式的图。
转换的方式有:图形制作工具生成 AVIF 格式;在线转换工具或网站;ffmpeg + AVIF encoder;我就转一张图而已,当时就用在线的网站进行转换,结果发现好多网站转换都超级慢,而且大多发生错误、失败。最终用这个网站 ezgif.com/ 转换成功。大小从 8M 减小到 2M 多。
我刚好有生成 gif 用的视频,mp4 格式。所以今天又用 mp4 直接转换了试试,看起来不错,这次只有 1M 而已,不过文件后缀是 .avifs。我查了一下它们的区别:
概念
文件扩展名为 .avif 和 .avifs,代表 AV1 Image File Format 和 AV1 Image File Format Sequence。大概 avif 表示静态图像格式,而 avifs 是动态版本。这里有一些介绍:fileinfo.com/extension/a… 网上关于 avifs 的介绍比较少。连 MDN 上也没有。
可以看到,AVIF 比 WebP 更小,压缩效果极佳。
但是直接将生成的 avifs 文件后缀改为 avif,也可以用,这可能跟它们的 MIME 类型有关。也就是说它们实际上的内容可能是一样的。
Web 应用
自然,因为 AVIF 格式较新,需要兼容更多浏览器。MDN 中也提到,应使用 <picture>
元素(或其他方法)提供格式的回退。
<picture class="fireworks">
<source
srcset="path-to-your-img.avifs"
type="image/avif"
/>
<source
srcset="path-to-your-img.webp"
type="image/webp"
/>
<source srcset="path-to-your-img.gif" type="image/gif" />
<img src="path-to-your-img.jpg" alt="" />
</picture>
HTML <picture>
元素通过包含零或多个 <source>
(en-US) 元素和一个 <img>
元素来为不同的显示/设备场景提供图像版本。浏览器会选择最匹配的子 <source>
元素,如果没有匹配的,就选择 <img>
元素的 src
属性中的 URL。然后,所选图像呈现在元素占据的空间中。
要决定加载哪个 URL,user agent 检查每个 <source>
的 srcset
(en-US)、media
(en-US) 和 type
(en-US) 属性,来选择最匹配页面当前布局、显示设备特征等的兼容图像。
<img>
元素有2个作用:
- 描述图片大小和其他的属性。
- 当所有 source 都不可用时,提供备选。
<picture>
的一般用途:
- 美术指导 (Art direction):根据不同的媒体条件
media
裁剪或修改图像(例如,在较小的显示器上加载具有太多细节的图像的简单版本)。 - 在不支持某些格式的情况下,提供替代图像格式。
- 通过加载最适合观看者显示的图像来节省带宽和加快页面加载时间。
type
属性允许你为 <source>
(en-US) 元素的 srcset
属性指向的资源指定一个 MIME 类型。如果用户代理不支持指定的类型,那么这个 <source>
(en-US) 元素会被跳过。
在以上的代码中,我们就是通过 type
属性来兼容多种格式类型的图片显示。这里我们不仅加上了 avifs,也用上了 WebP,最后是所有浏览器支持的 gif、jpg。
Vite 坑
在 Vite 项目中使用时,会有一个报错以及一堆乱码:
11:54:56 [vite] Internal server error: Failed to parse source for import analysis because the content contains invalid JS syntax. You may need to install appropriate plugins to handle the .avifs file format, or if it's an asset, add "**/*.avifs" to
assetsInclude
in your configuration. Plugin: vite:import-analysis
File: D:/work/xxx/xx.avifs:14:488
根据提示,在配置 vite.config.ts 中添加:
assetsInclude: ['**/*.avifs'],
这是将文件类型指定为静态资源处理 cn.vitejs.dev/config/shar… 内建支持的资源类型包括 avif,但是不包括 avifs。
资源请求
理想中,我们实现的 <picture>
方案只请求一张图片资源,实际上,Chrome 仍然请求了 img 的资源,
后来发现又没有了。。
再总结反思
从最初的想要在加载大的动画图过程中,先显示静止图,以优化体验,到最终不再考虑这一方面,而是通过优化图片的格式,大大缩小图片体积,加上兼容处理,达到优化效果。这一过程让人感慨,而其中值得权衡和总结的地方很多。
图片太大时,应该考虑 loading 的处理。
GIF 能够渐进渲染,所以又不太一样(如前面总结提到的优缺点)。
AVIF 不支持渐进式渲染,所以不像 GIF,如果图片体积较大,可能又要考虑加载过程 loading 的优化,或者应考虑使用支持渐进式渲染的格式。
AVIF 因为优秀的压缩效果,本身足够小,但是需要兼容处理。
不知道实际应用过程中会有什么问题,期待……
视频
更新:2023年12月21日16:46:22
其实用视频展示动画是一个比较好的选择,如果 GIF 动图是由视频转换来的,那一般比视频大的多,不如直接用视频。
video 标签,不写 controls 隐藏工具栏,autoplay 自动播放,loops 循环播放,CSS pointer-events: none; 禁用鼠标点击。