BlurHash 在web项目中的使用
什么是 BlurHash?
BlurHash 是一种图片占位符解决方案,它可以将图片编码为一个简短的字符串,然后在客户端解码为模糊的占位图像。这种技术特别适合在图片加载过程中提供更好的用户体验,避免页面出现空白或灰色占位符。
基本实现原理
BlurHash 通过傅里叶变换的思想,将图片的主要颜色和结构信息编码到一个短字符串中。这个字符串通常只有 20-30 个字符,但包含了足够的信息来生成一个视觉上令人愉悦的模糊预览图。
客户端实现示例
以下是在 Web 前端使用 BlurHash 的基本步骤:
1. 解码并显示 BlurHash
- 根据decode返回的像素数据,创建一个canvas,然后将像素数据绘制到canvas上,最后将canvas转换为DataURL 赋值给img标签的src属性,显示图片
- 具体的业务中需要根据实际情况进行实现,比如在微信小程序中,就不能使用document.createElement('canvas'),需要使用离线canvas替代
// 解码BlurHash为像素数据
const pixels = decode(blurhashString.value, 32, 32)
// 创建画布并绘制解码后的图像
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = 32
canvas.height = 32
// 创建ImageData对象
const imageData = ctx.createImageData(32, 32)
imageData.data.set(pixels)
// 将像素数据绘制到画布
ctx.putImageData(imageData, 0, 0)
// 将画布转换为DataURL 赋值给img标签的src属性,显示图片
decodedImageUrl.src = canvas.toDataURL()
2. 完整的 BlurHash 解码实现
- 以下是完整的 BlurHash 解码函数实现:
const digitCharacters = [
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "#", "$", "%", "*", "+", ",", "-", ".", ":", ";", "=", "?", "@", "[", "]", "^", "_", "{", "|", "}", "~",
];
const decode83 = (str: String) => {
let value = 0;
for (let i = 0; i < str.length; i++) {
const c = str[i];
const digit = digitCharacters.indexOf(c);
value = value * 83 + digit;
}
return value;
};
const sRGBToLinear = (value: number) => {
let v = value / 255;
if (v <= 0.04045) {
return v / 12.92;
} else {
return Math.pow((v + 0.055) / 1.055, 2.4);
}
};
const linearTosRGB = (value: number) => {
let v = Math.max(0, Math.min(1, value));
if (v <= 0.0031308) {
return Math.trunc(v * 12.92 * 255 + 0.5);
} else {
return Math.trunc((1.055 * Math.pow(v, 1 / 2.4) - 0.055) * 255 + 0.5);
}
};
const sign = (n: number) => (n < 0 ? -1 : 1);
const signPow = (val: number, exp: number) =>
sign(val) * Math.pow(Math.abs(val), exp);
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
this.message = message;
}
}
/**
* Returns an error message if invalid or undefined if valid
* @param blurhash
*/
const validateBlurhash = (blurhash: string) => {
if (!blurhash || blurhash.length < 6) {
throw new ValidationError(
"The blurhash string must be at least 6 characters"
);
}
const sizeFlag = decode83(blurhash[0]);
const numY = Math.floor(sizeFlag / 9) + 1;
const numX = (sizeFlag % 9) + 1;
if (blurhash.length !== 4 + 2 * numX * numY) {
throw new ValidationError(
`blurhash length mismatch: length is ${blurhash.length
} but it should be ${4 + 2 * numX * numY}`
);
}
};
const decodeDC = (value: number) => {
const intR = value >> 16;
const intG = (value >> 8) & 255;
const intB = value & 255;
return [sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)];
};
const decodeAC = (value: number, maximumValue: number) => {
const quantR = Math.floor(value / (19 * 19));
const quantG = Math.floor(value / 19) % 19;
const quantB = value % 19;
const rgb = [
signPow((quantR - 9) / 9, 2.0) * maximumValue,
signPow((quantG - 9) / 9, 2.0) * maximumValue,
signPow((quantB - 9) / 9, 2.0) * maximumValue,
];
return rgb;
};
export const decode = (
blurhash: string,
width: number,
height: number,
punch?: number
) => {
validateBlurhash(blurhash);
punch = (punch ?? 0) | 1;
const sizeFlag = decode83(blurhash[0]);
const numY = Math.floor(sizeFlag / 9) + 1;
const numX = (sizeFlag % 9) + 1;
const quantisedMaximumValue = decode83(blurhash[1]);
const maximumValue = (quantisedMaximumValue + 1) / 166;
const colors = new Array(numX * numY);
for (let i = 0; i < colors.length; i++) {
if (i === 0) {
const value = decode83(blurhash.substring(2, 6));
colors[i] = decodeDC(value);
} else {
const value = decode83(blurhash.substring(4 + i * 2, 6 + i * 2));
colors[i] = decodeAC(value, maximumValue * punch);
}
}
const bytesPerRow = width * 4;
const pixels = new Uint8ClampedArray(bytesPerRow * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0;
let g = 0;
let b = 0;
for (let j = 0; j < numY; j++) {
const basisY = Math.cos((Math.PI * y * j) / height);
for (let i = 0; i < numX; i++) {
const basis = Math.cos((Math.PI * x * i) / width) * basisY;
const color = colors[i + j * numX];
r += color[0] * basis;
g += color[1] * basis;
b += color[2] * basis;
}
}
let intR = linearTosRGB(r);
let intG = linearTosRGB(g);
let intB = linearTosRGB(b);
pixels[4 * x + 0 + y * bytesPerRow] = intR;
pixels[4 * x + 1 + y * bytesPerRow] = intG;
pixels[4 * x + 2 + y * bytesPerRow] = intB;
pixels[4 * x + 3 + y * bytesPerRow] = 255; // alpha
}
}
return pixels;
};
export default decode;
为什么选择 BlurHash?
你是否遇到过这些情况:
- 设计师每次看到他们精心设计的页面加载时,都因为图片还未加载而显示空白框而感到沮丧?
- 数据库工程师因为你想通过在数据中塞入小缩略图来解决这个问题而抓狂?
- 用户在网络不佳的环境下,看到页面上大片空白区域,体验极差?
BlurHash 的优势
使用 BlurHash 可以:
- 用漂亮的模糊预览状态替代枯燥的灰色占位框,让设计师开心
- 生成的 BlurHash 字符串足够短(通常只有 20-30 个字符),可以轻松作为 JSON 对象的字段存储在数据库中
- 实现简单,易于移植到新的编程语言和平台(Web、iOS、Android 等)
- 为用户提供流畅而有趣的体验,减少页面加载时的视觉跳动
- 相比于传统的缩略图方案,大大减少了数据传输量和存储需求
工作原理
BlurHash 的工作流程如下:
- 后端处理:
- BlurHash 接收一张图片
- 将图片转换为一个简短的字符串(仅 20-30 个字符!)
- 将这个字符串与图片一起存储
- 数据传输:
- 向客户端发送数据时,同时发送图片 URL 和 BlurHash 字符串
- 客户端展示:
- 客户端接收到 BlurHash 字符串
- 将字符串解码成占位图片
- 在实际图片加载期间显示这个占位图片
字符串的长度非常短,可以轻松适应任何数据格式。例如,可以作为 JSON 对象中的一个字段。
BlurHash 字符串格式说明
BlurHash 字符串的格式如下:
LlMF%n00%#MwS|WCWEM{R*bbWBbH
- 第一个字符:表示 X 和 Y 分量的数量
- 第二个字符:表示最大 AC 分量的值
- 剩余字符:编码的 DC 和 AC 分量
X 和 Y 分量越多,生成的模糊图像就越详细,但字符串也会越长。通常,使用 4x3 或 5x4 的分量就能得到不错的效果。
完整使用流程
1. 后端生成 BlurHash
在后端,你需要使用 BlurHash 编码库将图片转换为 BlurHash 字符串。以下是使用 Node.js 的示例:
const { encode } = require('blurhash');
const sharp = require('sharp');
const fs = require('fs');
async function encodeImageToBlurhash(path) {
const { data, info } = await sharp(path)
.raw()
.ensureAlpha()
.resize(32, 32, { fit: 'inside' })
.toBuffer({ resolveWithObject: true });
const blurhash = encode(
new Uint8ClampedArray(data),
info.width,
info.height,
4, // x components
3 // y components
);
return blurhash;
}
// 使用示例
async function main() {
const blurhash = await encodeImageToBlurhash('path/to/image.jpg');
console.log(blurhash); // 输出: LlMF%n00%#MwS|WCWEM{R*bbWBbH
// 将 blurhash 存储到数据库中
// db.saveImageWithBlurhash(imageUrl, blurhash);
}
main();
2. 前端使用 BlurHash
在前端,你需要使用 BlurHash 解码库将 BlurHash 字符串转换为图像。以下是一个 React 组件示例:
import React, { useEffect, useState, useRef } from 'react';
import { decode } from 'blurhash';
const BlurHashImage = ({ blurhash, src, width, height, alt }) => {
const [imgLoaded, setImgLoaded] = useState(false);
const canvasRef = useRef(null);
useEffect(() => {
if (!blurhash || !canvasRef.current) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
// 解码 BlurHash
const pixels = decode(blurhash, width, height);
// 创建 ImageData
const imageData = new ImageData(pixels, width, height);
// 绘制到 Canvas
ctx.putImageData(imageData, 0, 0);
}, [blurhash, width, height]);
return (
<div style={{ position: 'relative', width, height }}>
<canvas
ref={canvasRef}
width={width}
height={height}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
display: imgLoaded ? 'none' : 'block',
}}
/>
<img
src={src}
alt={alt}
onLoad={() => setImgLoaded(true)}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
opacity: imgLoaded ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
/>
</div>
);
};
export default BlurHashImage;
3. 在 Vue 中使用 BlurHash
<template>
<div class="blur-hash-image" :style="{ width: width + 'px', height: height + 'px' }">
<canvas ref="canvas" :width="width" :height="height" v-show="!imageLoaded"></canvas>
<img :src="src" :alt="alt" @load="onImageLoaded" :class="{ 'loaded': imageLoaded }">
</div>
</template>
<script>
import { ref, onMounted, watch } from 'vue';
import { decode } from './blurhash-decode'; // 使用上面提供的解码函数
export default {
name: 'BlurHashImage',
props: {
blurhash: {
type: String,
required: true
},
src: {
type: String,
required: true
},
width: {
type: Number,
default: 32
},
height: {
type: Number,
default: 32
},
alt: {
type: String,
default: ''
}
},
setup(props) {
const canvas = ref(null);
const imageLoaded = ref(false);
const renderBlurhash = () => {
if (!canvas.value || !props.blurhash) return;
const ctx = canvas.value.getContext('2d');
const pixels = decode(props.blurhash, props.width, props.height);
const imageData = ctx.createImageData(props.width, props.height);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
};
const onImageLoaded = () => {
imageLoaded.value = true;
};
onMounted(() => {
renderBlurhash();
});
watch(() => props.blurhash, () => {
renderBlurhash();
});
return {
canvas,
imageLoaded,
onImageLoaded
};
}
}
</script>
<style scoped>
.blur-hash-image {
position: relative;
}
canvas, img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
img {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
img.loaded {
opacity: 1;
}
</style>
性能考虑
BlurHash 解码需要一定的计算资源,特别是在移动设备上。以下是一些优化建议:
- 选择合适的输出尺寸 :通常 32x32 像素就足够作为模糊预览,没必要生成更大的尺寸
- 限制组件数量 :如果页面上有大量图片,考虑只为可见区域的图片生成 BlurHash 预览
- 缓存解码结果 :如果同一个 BlurHash 会被多次使用,可以缓存解码结果
- 使用 Web Worker :在复杂应用中,可以考虑将解码过程放到 Web Worker 中进行,避免阻塞主线程
在不同平台上的实现
BlurHash 已经有多种语言和平台的实现:
- JavaScript/TypeScript: blurhash
- React: react-blurhash
- Vue: vue-blurhash
- iOS/Swift: blurhash-swift
- Android/Kotlin: blurhash-android
- Python: blurhash-python
BlurHash 提供了一种优雅的解决方案,既满足了设计需求,又不会给数据库带来太大负担。它是图片加载优化的理想选择,特别适合以下场景:
- 图片密集型应用,如社交媒体、电商平台、图片库等
- 需要在弱网环境下提供良好用户体验的应用
- 对加载体验和视觉连续性有较高要求的产品
通过合理使用 BlurHash,你可以显著提升用户体验,减少用户在等待图片加载时的焦虑感,同时保持页面的视觉稳定性。
BlurHash 介绍及在项目中使用