图像占位符技术对比 - LQIP、BlurHash 和 SQIP 实现指南
什么是图像占位符 - 为什么比空白更好
图像占位符是在实际图像加载完成前显示的轻量级预览。与空白区域或加载动画相比,占位符提供内容预期,减少感知加载时间,防止布局偏移。
为什么需要占位符:
- 减少感知等待时间:用户看到模糊预览比看到空白更有耐心
- 防止 CLS:占位符预留空间,图像加载后不会推移内容
- 提供内容线索:用户可以预判图像内容,决定是否等待
三种主流方案:
- LQIP:极小尺寸的实际图像 (如 20×15 像素),放大模糊显示
- BlurHash:将图像编码为 20-30 字符的字符串,解码为模糊色块
- SQIP:使用 SVG 基本图形近似图像内容
延迟加载 (lazy loading) 会把视口之外的图像推后获取,因此滚动时会出现短暂的空白。这带来三个问题:一是 CLS (Cumulative Layout Shift) 恶化——图像加载完成的瞬间布局发生跳动,Core Web Vitals 的评分随之下降;二是感知上的迟缓,读者会觉得页面还没有准备好;三是内容语境的丢失,空白区域无法提示这里原本有什么。占位符正是为了同时缓解这三点而存在。
LQIP 实现 - 使用微型图像的预览显示
LQIP (Low Quality Image Placeholder) 是最直观的方案:生成一个极小尺寸的图像版本,内联到 HTML 中,放大显示时自然产生模糊效果。
生成方法:
// Sharp 生成 20px 宽的 LQIPconst lqip = await sharp('photo.jpg') .resize(20) .jpeg({ quality: 50 }) .toBuffer();const dataUri = `data:image/jpeg;base64,${lqip.toString('base64')}`;
大小:典型 LQIP 约 300-800 字节 (Base64 编码后)。可内联到 HTML 中,无需额外请求。
显示方式:使用 CSS filter: blur(20px) 增强模糊效果,配合 transform: scale(1.1) 隐藏边缘锯齿。图像加载完成后淡入替换。
优点:实现简单;保留原图的色彩分布和构图;无需额外库。缺点: Base64 编码增加 33% 体积;每张图像需要单独的 LQIP 数据。
LQIP 的做法因 Facebook 在 2015 年前后采用而广为人知。步骤有四步:①把原图缩小到宽 20-42 px (保持长宽比);②以 JPEG 质量 20-30 编码;③转成 Base64,以 data:image/jpeg;base64,... 的形式写进 <img> 的 src;④原图加载完成后把 src 换掉。宽 20 px、质量 20 的 JPEG 大约 300-600 字节,Base64 之后约 400-800 个字符,每张图可以控制在 1KB 以内。显示时用 CSS 掩盖压缩痕迹:.lqip { filter: blur(20px); transform: scale(1.1); },加载完成后切换到 .lqip.loaded { filter: blur(0); transform: scale(1); }。缩略图本身可以用 Sharp 或 ImageMagick 在构建时批量生成。
BlurHash 工作原理 - 创新的字符串图像表示
BlurHash 将图像编码为一个短字符串 (如 "LEHV6nWB2yk8pyo0adR*.7kCMdnj"),解码后生成模糊的色彩预览。由 Wolt 公司开发。
编码原理:将图像分解为离散余弦变换 (DCT) 的低频分量。编码参数 (componentX, componentY) 控制保留的频率分量数。典型设置 (4,3) 产生约 20-30 字符的哈希。
优势:
- 极小体积:20-30 字符,可存储在数据库字段或 JSON 中
- 无需存储额外文件
- 解码速度快:纯数学计算,无需图像解码器
- 多语言支持:官方提供 TypeScript、Swift、Kotlin 等实现
实现:
import { encode } from 'blurhash';// 编码 (服务端)const hash = encode(imageData, width, height, 4, 3);// 解码 (客户端)const pixels = decode(hash, 32, 32);
局限性:无法表示锐利边缘或文字;组件数增加会使字符串变长;需要预先知道图像宽高比。
BlurHash 把系数编码成 Base83 字符串。解码分三步:①解码 Base83 得到 DCT 系数;②做逆 DCT,通常还原成 32x32 的尺寸;③转换成 sRGB 后绘制到 Canvas 或 ImageData。4x3 个分量约 20 个字符 (约 20 字节),比 LQIP 的 300-600 字节紧凑 10-30 倍。JavaScript 侧的调用只有一行:const pixels = decode('LEHV6nWB2yk8pyo0adR*.7kCMdnj', 32, 32);,返回的是 Uint8ClampedArray (32*32*4 = 4096 个元素)。Instagram、Mastodon、Unsplash 都已采用;把这串字符放进 API 响应里,就能在图像 URL 尚未加载完成时先显示占位符。
SQIP 特点 - 使用 SVG 基本图形的艺术化占位符
SQIP (SVG-based LQIP) 使用 SVG 基本图形 (圆、椭圆、三角形等) 近似图像内容,产生具有艺术感的占位符效果。
工作原理:使用 Primitive 算法将图像分解为 N 个基本几何图形的叠加。每个图形的形状、位置、颜色和透明度通过优化确定,使组合结果尽可能接近原图。
生成方法:
npx sqip --input photo.jpg --output placeholder.svg --numberOfPrimitives 20
优势:
- SVG 格式,无限缩放不失真
- 视觉效果独特,具有艺术感
- 可用 CSS 动画增强效果
- 典型大小:800-2000 字节
劣势:
- 生成速度慢 (每张图像数秒)
- 不适合实时生成
- 图形数量少时近似度低,多时文件变大
- 维护状态不活跃
SQIP 由 Tobias Baldauf 在 2017 年发布,内部使用 Primitive (作者 Michael Fogleman)。处理流程有五步:①找出视觉上重要的区域;②放置 N 个图元 (默认 8 个);③用爬山法优化图元的形状、位置、颜色与不透明度;④对生成的 SVG 施加高斯模糊 (Gaussian blur) 滤镜;⑤转成 Base64 内联。8 个图元约 800-1200 字节。由于是矢量,在 Retina 显示屏上放大也不会出现模糊的像素块。代价是生成耗时,每张图需要 1-5 秒,图像多的站点构建时间会明显变长。此外 Node.js 的 sqip 包维护已经停滞,与较新的 Node.js 版本存在兼容性问题;替代做法是直接调用 primitive 命令行工具,再自行追加模糊滤镜。
对比与选择标准 - 为项目选择最佳技术
三种技术各有优劣,选择取决于项目的具体需求和约束。
对比表:
- 数据大小: BlurHash (20-30 字符) < LQIP (300-800B) < SQIP (800-2000B)
- 视觉质量: LQIP (最接近原图) > SQIP (艺术化) > BlurHash (纯色块)
- 生成速度: BlurHash (快) > LQIP (快) > SQIP (慢)
- 解码复杂度: LQIP (浏览器原生) < BlurHash (需要库) < SQIP (SVG 渲染)
选择建议:
- API 驱动的应用: BlurHash。字符串可轻松存储在数据库和 API 响应中
- 静态站点: LQIP。构建时生成,内联到 HTML,无需客户端库
- 设计导向的站点: SQIP。独特的视觉效果作为设计元素
- 性能极致优化: CSS 纯色背景。零额外字节,仅需提取主色调
按数据量排列:主色调 7 字节 (只有一个 CSS 颜色值) < BlurHash 20-30 字节 < LQIP 300-800 字节 < SQIP 800-1500 字节。按视觉质量排列则相反:SQIP > LQIP > BlurHash > 主色调。生成速度:主色调 (<1ms) > BlurHash (10-50ms) > LQIP (50-200ms) > SQIP (1-5 秒)。以 API 驱动的应用 (SNS、电商) 推荐 BlurHash——字符串足够短,可以直接随列表数据一起返回。
Next.js 和 Astro 集成实现 - 与现代框架的整合
将占位符技术集成到现代前端框架中的具体实现方法。
Next.js Image 组件: Next.js 的 <Image> 组件内置 placeholder="blur" 支持。静态导入的图像自动生成 LQIP;动态图像需要提供 blurDataURL。
<Image src={photo} placeholder="blur" />
Next.js + BlurHash:在 getStaticProps 中计算 BlurHash,通过 props 传递到组件。客户端使用 react-blurhash 或 Canvas API 解码显示。
Astro 集成: Astro 的 <Image> 组件支持通过 Sharp 在构建时生成 LQIP。自定义集成可在 Markdown 图像上自动添加占位符。
通用实现模式:
- 构建时:遍历所有图像,生成占位符数据,写入 JSON 或内联到 HTML
- 运行时:先显示占位符,监听图像
onload事件,加载完成后淡入切换 - CSS 过渡:
transition: opacity 0.3s实现平滑切换
Next.js 的 <Image> 通过 placeholder prop 原生支持 LQIP 与 BlurHash。动态图像可以在服务端用 plaiceholder 库生成:const { base64 } = await getPlaiceholder('/photo.jpg');,把得到的 base64 传给 placeholder 即可。Astro 没有直接的占位符支持,通常在构建时用 Sharp 处理:const buffer = await sharp('src/images/photo.jpg').resize(20).jpeg({ quality: 20 }).toBuffer();,再用 buffer.toString('base64') 内联。React 侧可以使用 react-blurhash,与 Intersection Observer 组合,在元素进入视口时再加载真实图像。内联的 Base64 会让 HTML 变大,所以 SSR 页面要留意初始 HTML 的传输量:一页有 50 张以上图像时,只对首屏附近的 5-10 张使用 LQIP,其余用主色调,是 Web 性能上的平衡点。