图片压缩一直是前端优化中至关重要的一环,尤其是在提高响应速度和减少网络传输时。尽管已有许多开源工具,如 Squoosh 和 TinyPNG,但在压缩效果和最新功能支持方面,现有工具常常滞后。本文将介绍如何基于 Rust 和 WebAssembly(WASM)实现一个自定义的 PNG 图片压缩库,并通过 WebWorker 实现异步操作,避免主线程阻塞。
为什么选择 imagequant
?
在众多的图片压缩库中,imagequant
是一个专注于 PNG 格式的高效压缩工具。其压缩效果优于其他常见的库,如 Squoosh 和 oxiPNG。遗憾的是,Squoosh 使用的 imagequant
版本较老,因此我们决定从头开始编译最新版本,生成符合需求的 WASM 包。
图片类型 | 压缩库 | 结论 |
---|---|---|
PNG | oxiPNG | squoosh使用的png压缩库,压缩率很一般,15-25%左右 |
PNG | imagequant | crates.io/crates/imag… 压缩效果≈70% squoosh编译出来的wasm太老了(v2.12.1), 需要自己再编译一次,最新的是(v4.3.0) |
JPEG | mozJPEG | github.com/mozilla/moz… 压缩效果≈80% |
WEBP | libwebp | github.com/webmproject… 压缩效果>90% |
SVG | libsvgo | github.com/svg/svgo 压缩效果10%~30% 原库svgo只支持node环境,libsvgo提供了浏览器的支持模式 |
AVIF | avif-serialize | github.com/packurl/was… 压缩效果>90%,但当前的兼容性差 squoosh使用的也比较旧, 且Figma不支持SharedArrayBuffer 重新编译了最新的avif-serialize |
压缩效果对比
为了确保压缩效果的质量,我们进行了大量的测试,使用对比工具(diffchecker)评估不同压缩工具和设置下的效果。经过测试,imagequant
提供了与 TinyPNG 类似的压缩效果,图片大小缩减至原图的 27.6%(减少 62.4%)。
使用 Rust 和 WASM 打包 imagequant
相关术语
squoosh:Chrome团队一个开源的客户端图片压缩网站
imagequant:一个处理png图像质量的库,可以减少图片的质量,本文用的是rust版本
wasm-pack:将rust打包成npm包的脚手架工具
crates:一个rust lib下载平台,类似于npm
WebAssembly的两种形态
首先简单介绍一下WebAssembly两种形态:
- 机器码格式
- 文本格式
这种文本形式更类似于处理器的汇编指令,因为WebAssembly本身是一门语言,一个小小的实例:
(module
(table 2 anyfunc)
(func $f1 (result i32)
i32.const 42)
(func $f2 (result i32)
i32.const 13)
(elem (i32.const 0) $f1 $f2)
(type $return_i32 (func (result i32)))
(func (export "callByIndex") (param $i i32) (result i32)
local.get $i
call_indirect $return_i32)
)
一般很少有人直接写文本格式,而是通过其他语言、或者是现存lib来编译成浏览器可用的wasm,这样很多客户端的计算模块只需简单处理都能很快转译成WASM在浏览器使用的模块,极大丰富了浏览器的使用场景。
接着我们先从一个入门实例开始,逐步到自己动手编译一个Rust模块。
配置 Rust 环境
首先,需要在 Rust 项目中添加 imagequant
和 wasm-bindgen
依赖,以便将 Rust 代码编译为 WASM:
[package]
name = "tinypng-lib-wasm"
version = "1.0.50"
edition = "2021"
author = ["wacrne"]
description = "TinyPNG Rust WASM Library"
license = "MIT"
[dependencies]
imagequant = { version = "4.2.0", default-features = false }
wasm-bindgen = "0.2.84"
console_error_panic_hook = { version = "0.1.7", optional = true }
lodepng = "3.7.2"
编写 Rust 代码
接下来,编写 Rust 代码以将 imagequant
的功能暴露给 JavaScript。使用 wasm-bindgen
让 Rust 函数可以被调用:
#[wasm_bindgen]
impl Imagequant {
#[wasm_bindgen(constructor)]
pub fn new() -> Imagequant {
Imagequant {
instance: imagequant::new(),
}
}
pub fn new_image(data: Vec<u8>, width: usize, height: usize, gamma: f64) -> ImagequantImage {
ImagequantImage::new(data, width, height, gamma)
}
// 省略其他实现
}
编译并打包为 NPM 包
通过 wasm-pack
工具将 Rust 代码编译为浏览器可用的 WASM 文件,并生成 NPM 包:
wasm-pack build --target bundler
打包生成npm package
使用 NPM 包
打包完成后,可以将 tinypng-lib-wasm
包作为依赖引入 JavaScript 项目,像下面这样使用:
import { Imagequant, ImagequantImage } from 'tinypng-lib-wasm';
// 获取图片信息
const { width, height, imageData } = await this.getImageBitInfo();
const uint8Array = new Uint8Array(imageData.data.buffer);
const image = new ImagequantImage(uint8Array, width, height, 0);
const instance = new Imagequant();
// 配置压缩质量
instance.set_quality(30, 85);
// 启动压缩
const output = instance.process(image);
const outputBlob = new Blob([output.buffer], { type: 'image/png' });
图片信息提取
使用 FileReader
和 canvas
获取图片的元数据,并将其传递给 imagequant
进行压缩:
// 获取图片信息:宽、高、像素数据、图片大小
const getImageBitInfo = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
// 创建一个 Image 对象
const img = new Image();
img.src = URL.createObjectURL(file);
img.onload = () => {
// 创建一个 canvas 元素
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('无法获取 canvas 上下文'));
return;
}
// 将图像绘制到 canvas 上
ctx.drawImage(img, 0, 0);
// 获取 ImageData
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const data = imageData.data; // Uint8ClampedArray
// 将 Uint8ClampedArray 转换为普通的 Uint8Array
const buffer = new Uint8Array(data).buffer;
// 确保缓冲区长度是 width * height * 4
const expectedLength = img.width * img.height * 4;
if (buffer.byteLength !== expectedLength) {
reject(new Error(`缓冲区长度不匹配:期望 ${expectedLength} 字节,但得到 ${buffer.byteLength} 字节`));
return;
}
resolve({
buffer,
width: img.width,
height: img.height,
size: file.size
});
// 释放对象 URL
URL.revokeObjectURL(img.src);
};
img.onerror = () => {
reject(new Error('图片加载失败'));
URL.revokeObjectURL(img.src);
};
});
};
演示一下,压缩效果还不错,对于质量,还可以调整相关的参数。目前的参数设置为 instance.set_quality(35, 88);
压缩效果可以媲美tinify。
压缩为原来的 27.6% (-62.4%)
tinify压缩效果(-61%)
其他类型图片压缩库打包
其他库squoosh比如webp、jpg、avif已经帮忙打包好了,svg有现成的npm库,因此较为简单。
使用 WebWorker 避免主线程阻塞
在压缩大图的时候,发现浏览器有点卡,周围的按钮的动效都无法正常运行,点也点不动。这是因为我们如果直接调用wasm会直接阻塞js主线程,既然是计算密集型的工作,这个时候就只能拿出非常适合这种场景的特性了:Worker。
配置 WebWorker
- 安装
worker-loader
以便在 Webpack 中使用 WebWorker。
npm install worker-loader
- 配置 Webpack 来支持 Worker 文件。
module.exports = {
module: {
rules: [
{
test: /.worker.js$/,
use: { loader: 'worker-loader' },
},
],
},
};
- 定义 WebWorker 代码:
// imageWorker.worker.js
import TinyPNG from 'tinypng-lib';
self.onmessage = async function (e) {
const { image, options } = e.data;
try {
const result = await TinyPNG.compressWorkerImage(image, options);
self.postMessage(result);
} catch (error) {
self.postMessage({ error: error.message });
}
};
在组件中使用 WebWorker
在组件中启动 WebWorker,接收压缩结果,并将文件信息发送给 Worker 进行处理:
import ImageWorker from './imageWorker.worker.js';
import TinyPNG from 'tinypng-lib';
export default {
data() {
return {
imgUrl: '',
compressResult: {},
};
},
mounted() {
this.worker = new ImageWorker();
this.worker.onmessage = (e) => {
const result = e.data;
if (result.error) {
console.error("Compression failed:", result.error);
} else {
const url = URL.createObjectURL(result.blob);
this.imgUrl = url;
this.compressResult = result;
}
};
},
methods: {
async uploadImg(e) {
const file = e.file;
const image = await TinyPNG.getImage(file);
this.worker.postMessage({
image,
options: { minimumQuality: 30, quality: 85 }
});
}
},
beforeDestroy() {
if (this.worker) {
this.worker.terminate();
}
}
};
总结
编译imagequant的过程比较坎坷,主要是rust的语言机制确实跟平常使用的语言不一样,需要学习的概念会多一些。不过获得的效果还是很不错的:
- 节省了服务器处理资源
- 节省了图片网络传输的时间
- 接入了WebWorker,可以并发执行任务且不阻塞
- 接入Service worker后可以做到离线使用
小广告:
- 开箱即用的图片压缩工具npm包:tinypng-lib
- 图片压缩工具wasm包:tinypng-lib-wasm
- 图片压缩工具使用:tinypng.wcrane.cn