跳到主要内容

基于顶级编解码器实现纯前端高效图片压缩

勾玉aniki
博客作者,py&go后端开发,爱好动漫。邮箱tangssst@qq.com

Google的Squoosh项目封装了MozJPEG、libwebp、rust Oxipng等顶级图像编解码器,但是使用依赖于Node.js,而jSquash项目对此进行了二次封装,将底层编解码器以WebAssembly的形式提供,实现不依赖node的纯前端的压缩方式。 经测试可以将10mb的png压缩为500kb的jpg,肉眼画质无损失,95%的优化。

文末会给出我的使用代码。github项目地址也在文末。

jSquash

jSquash是一个轻量级、基于WebAssembly(WASM)的图像处理库,旨在为浏览器和V8运行时(如Cloudflare Workers)提供高效的图像编解码能力。其设计来源于Squoosh,强调浏览器兼容性和模块化。特点:

  • 专注于浏览器和Web Worker,适合前端环境。
  • 模块化设计:支持多种图像格式(如@jSquash/jpeg、@jSquash/png、@jSquash/webp等),按需引入。
  • 无动态代码执行:可在严格环境中运行,如Cloudflare Workers。

而WASM优势:

  • 接近原生代码的执行速度,适合计算密集型任务如图像压缩。
  • 无需依赖特定运行时环境,浏览器即可运行。
  • WASM运行在沙箱环境中,适合严格的执行环境。

比如@jSquash/jpeg使用MozJPEG库进行JPEG图像的编解码,@jSquash/webp基于libwebp实现WebP格式支持。通过Emscripten编译为WASM模块,并通过JavaScript接口暴露给开发者。

jSquash采用ES Module(ESM)格式,所有模块(如@jSquash/avif、@jSquash/png)均为独立包,可按需引入,减少了打包体积,适合Vite、Webpack这些前端构建工具。

jSquash也支持通过CDN直接使用(大陆环境不稳定):

import { decode } from 'https://unpkg.com/@jsquash/jpeg?module';

处理流程:

  1. 解码(Decode):将输入的图像二进制数据(ArrayBuffer)解码为原始RGB图像数据(ImageData)。例如,@jSquash/jpeg的decode方法将JPEG文件转换为ImageData对象。
  2. 处理(可选):支持调整大小(@jSquash/resize)或优化(如@jSquash/oxipng)。调整大小支持多种算法(如Lanczos3、Magic Kernel)。
  3. 编码(Encode):将处理后的ImageData编码为目标格式的二进制数据。例如,@jSquash/webp的encode方法生成WebP格式的ArrayBuffer。

jSquash的WASM模块通常由生成的胶水代码(glue code)自动初始化,支持大多数Web打包工具

图像格式支持

每种格式由独立的模块实现:

  • @jSquash/avif - 使用 libavif 库的 AVIF 图像编码器和解码器
  • @jSquash/jpeg - 使用 MozJPEG 库的 JPEG 图像编码器和解码器
  • @jSquash/jxl - 使用 libjxl 库的 JPEG XL 图像编码器和解码器
  • @jSquash/oxipng - 使用 Oxipng 的 PNG 图像优化器
  • @jSquash/png - 使用 rust PNG crate 的 PNG 图像编解码器
  • @jSquash/qoi - 使用官方库的 "相当好的图像格式" 编解码器
  • @jSquash/resize - 使用 rust resize、hqx 和 magic-kernel 库的图像缩放工具。支持降缩和升缩。
  • @jSquash/webp - 使用 libwebp 的 WebP 图像编码器和解码器

完整见官方readme,按需npm install即可(install的是@jsquash/xx而不是@jSquash/xx,要小写)。

代码

只包括图像处理的核心部分,图像是上传还是直接读取本地自行处理。 注意每个模块的使用方式是不一样的,比如oxipng的输入类型为arrayBuffer,而MozJpeg为imageData,具体需要查看每个封装模块的readme。

jpeg、webp: 这里encode可以传入配置quality,默认为75(0-100),具体库配置不同。png配置为level(1-6)

import { decode } from '@jsquash/jpeg';
import { encode } from '@jsquash/webp';

async function convertToWebP(url) {
try {
const response = await fetch(url);
const imageBuffer = await response.arrayBuffer();
// 或者直接处理blob对象
// file.arrayBuffer()
const imageData = await decode(imageBuffer);
// 这里encode可以传入配置{ quality },默认为75(0-100),具体库配置不同。png配置为level(1-6)
const webpBuffer = await encode(imageData);
const blob = new Blob([webpBuffer], { type: 'image/webp' });
return URL.createObjectURL(blob);
} catch (error) {
console.error('转换失败:', error);
}
}

rust oxipng:

这里用canvas解码,因为这个库没提供解码函数。

      const bitmap = await createImageBitmap(file);
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
bitmap.close();
return imageData;

编码:

import {optimise as encodePNG} from '@jsquash/oxipng';

let buffer = await encodePNG(buffer, { level: 2 }),
return new Blob([buffer], {type: 'image/png'});

没有明确要求(比如png保留透明通道)的话建议输出格式转为jpeg,能大幅度减小体积。

注意

vite优化的时候要避开这些库,不能影响wasm,否则会导致无法生效:

export default defineConfig({
optimizeDeps: {
exclude: ["@jsquash/jpeg", "@jsquash/oxipng", "@jsquash/webp"]
}
})

参考资料