﻿---
title: 图片渐进式加载方法探究
date: 2026-01-21
excerpt: 深入解析 LQIP、BlurHash、ThumbHash等图片渐进式加载方案
tags:
  - Web
  - LQIP
  - Image
  - UIUX
updated: 2026-05-23 15:36:36
i18n:
  en: /en/image_loading
translation: 2
---
在现代 Web 开发中，开发者通过加载动画、骨架屏或**模糊/低清占位图**等策略，试图让应用在视觉上比实际加载速度更快。

## 传统派 🚬

假设有如下HTML

```html
<img src="https://assets.vluv.space/4k-wallpaper.webp" alt="404"/>
```

这种写法虽然简单，不过在弱网环境下，要长时间等待图片加载，可参考如下甘特图

```mermaid
gantt
    title 时间值并不严谨,仅供参考
    dateFormat s
    axisFormat %S
    开始加载图片 : vert, v1, 1,1
    原图加载完成 : vert, v2, 10,1
    获取HTML          :active, a1, 0, 1
    空白/纯色占位图                  :crit, b1, 1, 10
    展示原图 : 10,11
```

即使现在通5G了，也可能有：

- 博客服务器带宽小，包括但不限于CDN限速/源站限速 etc.
- 图片体积大 👍
- 不排除个人喜好，就爱瞎折腾

总之，优化还是有用的。

## LQIP 🌄

LQIP（Low-Quality Image Placeholder）技术由 **Facebook** 团队在 2015 年推广[^1]，最初是为了解决移动端封面图加载慢的问题。

该方案的核心思路并不复杂:

- 服务端：在保持原始宽高比的前提下，对图片进行大幅压缩，生成一张体积极小的低分辨率图片；
- 客户端：先将该低分辨率图片拉伸至目标尺寸，并叠加高斯模糊（Gaussian Blur）滤镜进行展示；

> 在FaceBook团队的技术文章中，为了极致的压缩，其还在客户端中预存标准的JPEG头，服务器仅发送版本号、宽高及约 200 字节的图像数据。

从时间轴上看，LQIP 的价值主要体现在 “原图加载完成之前” 的这段空窗期：用户并非看到纯色占位图，而是一个与最终内容高度一致的模糊预览。这种方式可以为用户提供一个较为丝滑视觉反馈，目前博客的封面图就是使用了该方案

```mermaid
gantt
    dateFormat s
    axisFormat %S
    开始加载图片 : vert, v1, 1,1
    原图加载完成 : vert, v2, 10,1
    获取HTML          :active, a1, 0, 1
    请求低分辨率占位图                    :crit, c1, 1, 3
    显示模糊占位图 :done, c1, 3, 10
    展示原图 : 10,11
```

下面的两个示例[^2]可以直观感受到 LQIP 带来的体验提升

<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem;">
  <!-- Column 1-->
  <div style="flex: 1;">
<video autoplay="" muted="" loop="" playsinline="" src="https://stream.mux.com/tteqFs02cDDEqq00ZwBWF001SPmtazLIjwx/high.mp4" style="width: 400px; border-radius: 20px"></video>
  </div>
  <!-- Column 2-->
  <div style="flex: 1;">
<video autoplay="" muted="" loop="" playsinline="" src="https://stream.mux.com/GgOuQbQDxoFOSKe00QG5P1dLSd00sS5nOj/high.mp4" style="width: 400px; border-radius: 20px"></video>
  </div>
</div>

## ThumbHash & BlurHash 🫨

在 LQIP 之外，业界还提出了 **ThumbHash** 与 **BlurHash** 两种方案，它们在<abbr class="abbr" title="User Experience">UX</abbr>层面试图解决的是同一个问题：在原图尚未加载完成前，尽可能早地呈现一个与最终内容“形似”的视觉占位。不过，这两者的实现路径与 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 解析完成后才开始执行，这意味着占位图的展示本身就可能出现**可感知的延迟**。

一种折中的做法是：在服务端将哈希还原为 [[base64_encoding|Base 64]] 图片，并直接**嵌入到HTML**中

```mermaid
gantt
    dateFormat s
    axisFormat %S
    开始加载图片 : vert, v1, 1,1
    原图加载完成 : vert, v2, 10,1

    获取HTML          :active, a1, 0, 1
    解析 HTML 并等待 JS 库加载         :crit, d1, 1, 3
    客户端 JS 解码哈希字符串           :d2, 3, 4
    显示模糊占位图 (解码后)            :done, d3, 4, 10
    展示原图 : 10,11

    section HTML内嵌Base64
    获取HTML          :active, 0, 1
    展示DataURL        :done, e1, 1, 10
    展示原图 : 10,11
```

## 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.
>
> <cite>Justin Greer</cite>

然而在 [Blur Up Thumbs](https://blur-up-thumbs.vercel.app/) 页面直观看了体积对比，才意识到自己其实走了点弯路。不过项目已经形成了路径依赖，也懒得推倒重来，于是退而求其次，把这些 data URL 转换成 WebP。最终，400 张图片的总体积降到了 约 110 KB，平均 275 Byte/张。

不算最优解，但是：🧄🐦🧄🐦（发出武汉声音）

![蒜鸟|200x200](https://assets.vluv.space/蒜鸟.avif)

## References

<script data-swup-reload-script type="module" src="/js/components/accordion.js"></script>
<x-accordion>
  <accordion-item title="Cover Image's HTML Structure In this Blog">

```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>
```

  </accordion-item>
  <accordion-item title="Image in article's HTML Structure">

```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>
```

  </accordion-item>
</x-accordion>

[^1]: 参考链接 [The technology behind preview photos - Engineering at Meta](https://engineering.fb.com/2015/08/06/android/the-technology-behind-preview-photos/)
[^2]: 视频Source: [A clear look at blurry image placeholders on the web | Mux](https://www.mux.com/blog/blurry-image-placeholders-on-the-web) by  [Wesley Luyten](https://www.mux.com/team/wesley-luyten)
[^3]: 原理摘自 [BlurHash](https://blurha.sh/)
[^4]: [ThumbHash: A very compact representation of an image placeholder](https://evanw.github.io/thumbhash/)
