图片渐进式加载方法探究
在现代 Web 开发中,开发者通过加载动画、骨架屏或模糊/低清占位图等策略,试图让应用在视觉上比实际加载速度更快。
传统派 🚬
假设有如下HTML<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <!-- 没有任何占位处理的传统加载方式 --> <img src="https://assets.vluv.space/4k-wallpaper.webp" alt="404"/> </body></html>
这种写法虽然简单,但对于处于 2G 或弱网环境的用户来说,这往往意味着糟糕的第一印象。通过下方的甘特图,可以直观地注意到存在8秒的空窗期,期间用户无法观看到图片
即使现在通5G了,也可能有:
- 博客服务器带宽🟰小水管
- CDN限速/源站限速 etc.
- 站内都是近百兆的高清好图 👍
- 不排除个人喜好,就爱瞎折腾
总之,优化还是有用的。
LQIP 🌄
LQIP(Low-Quality Image Placeholder)技术由 Facebook 团队在 2015 年推广[1],最初是为了解决移动端封面图加载慢的问题。
该方案的核心思路并不复杂:
- 服务端:在保持原始宽高比的前提下,对图片进行大幅压缩,生成一张体积极小的低分辨率图片;
- 客户端:先将该低分辨率图片拉伸至目标尺寸,并叠加高斯模糊(Gaussian Blur)滤镜进行展示;
在FaceBook团队的技术文章中,为了极致的压缩,其还在客户端中预存标准的JPEG头,服务器仅发送版本号、宽高及约 200 字节的图像数据。
从时间轴上看,LQIP 的价值主要体现在 “原图加载完成之前” 的这段空窗期:用户并非看到纯色占位图,而是一个与最终内容高度一致的模糊预览。这种方式可以为用户提供一个较为丝滑视觉反馈,目前博客的封面图就是使用了该方案
下面的两个示例[2]可以直观感受到 LQIP 带来的体验提升
ThumbHash & BlurHash 🫨
在 LQIP 之外,业界还提出了 ThumbHash 与 BlurHash 两种方案,它们在UX层面试图解决的是同一个问题:在原图尚未加载完成前,尽可能早地呈现一个与最终内容“形似”的视觉占位。不过,这两者的实现路径与 LQIP 不同:
技术原理
BlurHash 会提取一张图片,并生成一段仅 20–30 个字符的短字符串来代表这张图片的占位符。你在服务端的后台完成这一操作,并将该字符串与图片一起存储。当你向客户端发送数据时,会同时发送图片的 URL 和 BlurHash 字符串。客户端收到后,把字符串解码成一张模糊图片,在网络加载真实图片期间进行展示。该字符串足够短,可以轻松嵌入你使用的任何数据格式,例如直接作为 JSON 对象中的一个字段。[3]
ThumbHash[4],可以视为 BlurHash 的一次演进:
- Encodes more detail in the same space
- Also encodes the aspect ratio
- Gives more accurate colors
- Supports images with alpha
Limitation & Workaround
尽管 ThumbHash 与 BlurHash 在概念上非常优雅,但在 Web 场景中仍然存在一些绕不开的现实约束,即对 JavaScript 的强依赖
浏览器无法直接解析哈希字符串并渲染图像,必须依赖 JavaScript 解码库。而 JS 通常是在 HTML 解析完成后才开始执行,这意味着占位图的展示本身就可能出现可感知的延迟。
一种折中的做法是:在服务端将哈希还原为 Base 64 图片,并直接嵌入到HTML中
Summary
个人博客的封面图采用了 LQIP 方案,而文章内图片则使用了上面提到的折中做法。全站共计 400 余张图片,PNG 格式的 data URL 总体积约 1 MB,平均每张 2.5 KB。相比thumbhash/blurhash 20–30 Byte 的字符串,体积膨胀了 100 倍以上(但是减少了JavaScript依赖 😎)
If you transform the image to a blurhash and back to an image, then send the image to the customer, what you’ve done is treat BlurHash as a preprocessing filter that loses the majority of its benefits.
Justin Greer
然而在 Blur Up Thumbs 页面直观看了体积对比,才意识到自己其实走了点弯路。不过项目已经形成了路径依赖,也懒得推倒重来,于是退而求其次,把这些 data URL 转换成 WebP。最终,400 张图片的总体积降到了 约 110 KB,平均 275 Byte/张。
不算最优解,但是:🧄🐦🧄🐦(发出武汉声音)
References
Cover Image HTML
<style>.cover-image { position: relative; display: block; overflow: hidden; height: 380px; img { display: block; width: 100%; height: 100%; object-fit: cover; } .cover-lqip { position: absolute; top: 0; left: 0; z-index: 1; filter: blur(10px); } .cover-origin { position: relative; z-index: 2; }}</style><a href="xxx" class="cover-image"> <img class="cover-lqip" src={lqip_src} alt="placeholder" /> <img class="cover-origin" src={cover} srcset="xxx" alt="xxx" decoding="async" loading="lazy"/></a>
Article Image HTML
<div class="pic" style="aspect-ratio: 3638 / 2032; background-image: url(data:image/webp;base64,U...==); background-repeat: no-repeat; background-size: cover; width: 100%; max-width: 3638px; position: relative; overflow: hidden;"> <img onload="this.style.opacity=1; setTimeout(() => { this.parentElement.style.backgroundImage='none'; }, 600);" src="https://assets.vluv.space/xxx.webp" srcset="xxx" style="object-fit: cover; opacity: 1; width: 100%; height: 100%; transition: opacity 0.6s ease-in-out;" alt="xxx" decoding="async" loading="lazy"></div>
图片渐进式加载方法探究
