基于 Squoosh WASM 的浏览器端图片转换库

作者:jump_jump日期:2026/1/7

在 Web 开发中,图片处理是一个常见需求。传统方案要么依赖服务端处理,要么使用 Canvas API,但前者增加服务器负担,后者在压缩质量上不尽人意。Google 的 Squoosh 项目提供了基于 WASM 的高质量图片编解码器,但直接使用比较繁琐。

于是我封装了 use-squoosh,一个零依赖的浏览器端图片转换库,通过 CDN 按需加载编解码器,开箱即用。

为什么需要这个库

现有方案的局限性

方案优点缺点
服务端处理稳定可靠增加服务器负担、网络开销
Canvas API无依赖JPEG 质量差、不支持 WebP 编码
直接使用 @jsquash质量好需要手动管理多个包、配置 WASM
在线工具简单隐私风险、批量处理不便

Canvas 的质量问题

Canvas 的 toBlob()toDataURL() 方法虽然简单,但存在明显缺陷:

1// Canvas 方式
2canvas.toBlob(callback, 'image/jpeg', 0.8);
3

问题:

  1. JPEG 编码器质量较差,同等文件大小下清晰度不如专业编码器
  2. 不支持 WebP 编码(部分旧浏览器)
  3. 无法精确控制编码参数

Squoosh 的优势

Squoosh 是 Google Chrome Labs 开发的图片压缩工具,其核心是一系列编译为 WASM 的高性能编解码器:

  • MozJPEG:Mozilla 优化的 JPEG 编码器,同等质量下文件更小
  • libwebp:Google 官方 WebP 编解码器
  • OxiPNG:Rust 编写的 PNG 优化器

@jsquash 将这些编解码器封装为独立的 npm 包,但直接使用需要:

  1. 安装多个包(@jsquash/webp、@jsquash/png、@jsquash/jpeg)
  2. 手动处理 WASM 文件加载
  3. 管理编解码器的初始化

use-squoosh 解决了这些问题。

核心设计思路

零依赖 + CDN 加载

最核心的设计决策是:不打包编解码器,运行时从 CDN 加载

1// 编解码器通过动态 import  CDN 加载
2const url = `${cdnConfig.baseUrl}/@jsquash/webp@${version}/encode.js`;
3const module = await import(/* @vite-ignore */ url);
4

好处:

  1. 库本身体积极小(< 5KB gzipped)
  2. 编解码器按需加载,不使用的格式不会下载
  3. 利用 CDN 缓存,多项目共享同一份 WASM

加载时机:

  • 首次调用转换函数时加载对应格式的编解码器
  • 加载后缓存到 window 对象,页面内复用
  • 支持预加载关键格式

Promise 缓存避免竞态

并发场景下可能同时触发多次加载:

1// 错误示例:可能重复加载
2async function getEncoder() {
3  if (!cache.encoder) {
4    cache.encoder = await import(url);  // 并发时会多次触发
5  }
6  return cache.encoder;
7}
8

解决方案是缓存 Promise 而非结果:

1// 正确示例:缓存 Promise
2async function getCodec(type: CodecType): Promise<any> {
3  const cache = getCache();
4  if (!cache[type]) {
5    // 缓存 Promise 本身,而非 await 后的结果
6    cache[type] = import(/* @vite-ignore */ url);
7  }
8  const module = await cache[type];
9  return module.default;
10}
11

这样即使并发调用,也只会触发一次网络请求。

全局缓存支持多项目共享

编解码器挂载到 window 对象:

1function getCache(): CodecCache {
2  if (typeof window !== "undefined") {
3    const key = cdnConfig.cacheKey;
4    if (!(window as any)[key]) {
5      (window as any)[key] = createEmptyCache();
6    }
7    return (window as any)[key];
8  }
9  return moduleCache;  // 非浏览器环境回退
10}
11

好处:

  • 同一页面多个组件/库使用 use-squoosh,共享编解码器
  • 页面导航不重新加载(SPA 场景)
  • 可配置 cacheKey 实现隔离

实现细节

格式自动检测

当输入是 BlobFile 时,自动从 MIME 类型检测格式:

1const FORMAT_MAP: Record<string, ImageFormat> = {
2  "image/png": "png",
3  "image/jpeg": "jpeg",
4  "image/webp": "webp",
5  // 同时支持扩展名
6  png: "png",
7  jpeg: "jpeg",
8  jpg: "jpeg",
9  webp: "webp",
10};
11
12export async function convert(
13  input: ArrayBuffer | Blob | File,
14  options: ConvertOptions = {},
15): Promise<ArrayBuffer> {
16  let buffer: ArrayBuffer;
17  let fromFormat = options.from;
18
19  if (input instanceof Blob || input instanceof File) {
20    buffer = await input.arrayBuffer();
21    // 自动检测格式
22    if (!fromFormat && input.type) {
23      fromFormat = getFormat(input.type) ?? undefined;
24    }
25  } else {
26    buffer = input;
27  }
28
29  // ...
30}
31

解码 -> 编码流程

图片转换本质是:解码为 ImageData → 编码为目标格式。

1export async function decode(
2  buffer: ArrayBuffer,
3  type: ImageFormat,
4): Promise<ImageData> {
5  switch (type.toLowerCase()) {
6    case "png": {
7      const decoder = await getPngDecoder();
8      return decoder(buffer);
9    }
10    case "jpeg":
11    case "jpg": {
12      const decoder = await getJpegDecoder();
13      return decoder(buffer);
14    }
15    case "webp": {
16      const decoder = await getWebpDecoder();
17      return decoder(buffer);
18    }
19    default:
20      throw new Error([`Unsupported decode type: ${type}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.type.md));
21  }
22}
23
24export async function encode(
25  imageData: ImageData,
26  type: ImageFormat,
27  options: { quality?: number } = {},
28): Promise<ArrayBuffer> {
29  switch (type.toLowerCase()) {
30    case "png": {
31      const encoder = await getPngEncoder();
32      return encoder(imageData);  // PNG 无损,不需要 quality
33    }
34    case "jpeg":
35    case "jpg": {
36      const encoder = await getJpegEncoder();
37      return encoder(imageData, { quality: options.quality ?? 75 });
38    }
39    case "webp": {
40      const encoder = await getWebpEncoder();
41      return encoder(imageData, { quality: options.quality ?? 75 });
42    }
43    default:
44      throw new Error([`Unsupported encode type: ${type}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.type.md));
45  }
46}
47

CDN 配置系统

支持自定义 CDN 地址和版本:

1export interface CDNConfig {
2  baseUrl?: string;      // CDN 基础路径
3  webpVersion?: string;  // @jsquash/webp 版本
4  pngVersion?: string;   // @jsquash/png 版本
5  jpegVersion?: string;  // @jsquash/jpeg 版本
6  cacheKey?: string;     // window 缓存 key
7}
8
9const defaultCDNConfig: Required<CDNConfig> = {
10  baseUrl: "https://cdn.jsdelivr.net/npm",
11  webpVersion: "1.5.0",
12  pngVersion: "3.1.1",
13  jpegVersion: "1.6.0",
14  cacheKey: "__ImageConverterCache__",
15};
16

智能缓存清除: 只有 CDN 相关配置变更时才清除缓存:

1export function configure(config: CDNConfig): void {
2  const cdnKeys: (keyof CDNConfig)[] = [
3    "baseUrl", "webpVersion", "pngVersion", "jpegVersion",
4  ];
5
6  // 只有这些字段变更才清除缓存
7  const needsClearCache = cdnKeys.some(
8    (key) => key in config && config[key] !== cdnConfig[key],
9  );
10
11  cdnConfig = { ...cdnConfig, ...config };
12
13  if (needsClearCache) {
14    clearCache();
15  }
16}
17

编解码器 URL 生成

统一管理编解码器的包名、版本和文件路径:

1const codecConfig: Record<
2  CodecType,
3  { pkg: string; version: keyof CDNConfig; file: string }
4> = {
5  webpEncoder: { pkg: "@jsquash/webp", version: "webpVersion", file: "encode.js" },
6  webpDecoder: { pkg: "@jsquash/webp", version: "webpVersion", file: "decode.js" },
7  pngEncoder: { pkg: "@jsquash/png", version: "pngVersion", file: "encode.js" },
8  pngDecoder: { pkg: "@jsquash/png", version: "pngVersion", file: "decode.js" },
9  jpegEncoder: { pkg: "@jsquash/jpeg", version: "jpegVersion", file: "encode.js" },
10  jpegDecoder: { pkg: "@jsquash/jpeg", version: "jpegVersion", file: "decode.js" },
11};
12
13async function getCodec(type: CodecType): Promise<any> {
14  const cache = getCache();
15  if (!cache[type]) {
16    const { pkg, version, file } = codecConfig[type];
17    const url = `${cdnConfig.baseUrl}/${pkg}@${cdnConfig[version]}/${file}`;
18    cache[type] = import(/* @vite-ignore */ url);
19  }
20  const module = await cache[type];
21  return module.default;
22}
23

使用方式

基本使用

1import { convert, pngToWebp, compress } from 'use-squoosh';
2
3// 文件选择器获取图片
4const file = input.files[0];
5
6// PNG  WebP
7const webpBuffer = await pngToWebp(file, { quality: 80 });
8
9// 通用转换
10const result = await convert(file, {
11  from: 'png',    // Blob/File 可省略,自动检测
12  to: 'webp',
13  quality: 85
14});
15
16// 压缩(保持原格式)
17const compressed = await compress(file, {
18  format: 'jpeg',
19  quality: 70
20});
21

配置 CDN

1import { configure } from 'use-squoosh';
2
3// 使用 unpkg
4configure({ baseUrl: 'https://unpkg.com' });
5
6// 使用自托管 CDN
7configure({ baseUrl: 'https://your-cdn.com/npm' });
8
9// 锁定特定版本
10configure({
11  webpVersion: '1.5.0',
12  pngVersion: '3.1.1',
13  jpegVersion: '1.6.0'
14});
15

预加载优化首屏

1import { preload, isLoaded } from 'use-squoosh';
2
3// 页面加载时预加载常用格式
4await preload(['webp', 'png']);
5
6// 检查加载状态
7if (isLoaded('webp')) {
8  // WebP 编解码器已就绪
9}
10

工具函数

1import { toBlob, toDataURL, download } from 'use-squoosh';
2
3const buffer = await pngToWebp(file);
4
5// 转为 Blob
6const blob = toBlob(buffer, 'image/webp');
7
8// 转为 Data URL(用于 img.src)
9const dataUrl = await toDataURL(buffer, 'image/webp');
10
11// 触发下载
12download(buffer, 'converted.webp', 'image/webp');
13

自托管 CDN

如果不想依赖公共 CDN,可以自托管编解码器文件。

目录结构要求

1your-cdn.com/npm/
2  @jsquash/
3    webp@1.5.0/
4      encode.js
5      decode.js
6    png@3.1.1/
7      encode.js
8      decode.js
9    jpeg@1.6.0/
10      encode.js
11      decode.js
12

获取文件

从 npm 下载对应版本:

1# 下载 @jsquash 
2npm pack @jsquash/webp@1.5.0
3npm pack @jsquash/png@3.1.1
4npm pack @jsquash/jpeg@1.6.0
5
6# 解压并部署到 CDN
7

配置使用

1configure({
2  baseUrl: 'https://your-cdn.com/npm',
3  webpVersion: '1.5.0',
4  pngVersion: '3.1.1',
5  jpegVersion: '1.6.0'
6});
7

压缩效果对比

以一张 1920x1080 的 PNG 截图为例:

输出格式Quality文件大小压缩率
原始 PNG-2.1 MB-
WebP80186 KB91%
WebP90312 KB85%
JPEG80245 KB88%
JPEG90398 KB81%

WebP 在同等视觉质量下,文件大小比 JPEG 小约 25-35%。

浏览器兼容性

需要支持 WebAssembly 和动态 import:

浏览器最低版本
Chrome57+
Firefox52+
Safari11+
Edge16+

覆盖全球 95%+ 的用户。

与其他方案对比

特性use-squooshbrowser-image-compression直接使用 @jsquash
包大小< 5KB~50KB~2KB × 6
运行时依赖CDN 加载打包在内需手动配置
WebP 支持
PNG 优化
质量控制
自动格式检测
预加载需手动
自定义 CDN
TypeScript

总结

use-squoosh 通过以下设计实现了易用的浏览器端图片转换:

  1. 零依赖设计:编解码器按需从 CDN 加载,库本身极轻量
  2. Promise 缓存:避免并发场景重复加载
  3. 全局共享:多组件/项目复用编解码器
  4. 灵活配置:支持自定义 CDN 和版本锁定
  5. TypeScript:完整类型定义,开发体验好

项目已开源:github.com/wsafight/us…

欢迎提出 issue 和 PR。

参考资料

  • Squoosh - Google 的在线图片压缩工具
  • jSquash - Squoosh 编解码器的 npm 封装
  • WebAssembly - 浏览器端高性能运行时

基于 Squoosh WASM 的浏览器端图片转换库》 是转载文章,点击查看原文


相关推荐


微调—— LlamaFactory工具:使用WebUI微调
华如锦2025/12/29

启动web Ui面板 进入到LLaMA-Factory目录下,执行以下命令启动web ui面板: cd LLaMA-Factory llamafactory-cli webui llamafactory-cli webui 进入web ui面板 微调前准备 1. 数据准备 LLaMA-Factory 自带数据集以 .json 格式存放在项目根目录的 LLaMA-Factory/data 文件夹中,在图形化微调界面中可直接通过下拉框选择这些数据集。)。


Python入门指南(五) - 为什么选择 FastAPI?
吴佳浩2025/12/20

Python入门指南(五) - 为什么选择 FastAPI? 欢迎来到Python入门指南的第五部分!在上一章中,我们完成了Python开发环境的初始化配置。现在,让我们进入实战阶段——选择合适的Web框架来构建我们的API服务。 本章将深入对比 Flask 和 FastAPI,帮助你理解为什么在现代Python开发中,FastAPI正在成为越来越多开发者的首选。 ** 为什么需要Web框架?** 在进入对比之前,先理解Web框架的核心作用: 处理HTTP请求和响应:接收用户请求,返回处理


【转载】为什么我们选择GPT-5.2作为Augment Code Review的模型
是魔丸啊2025/12/12

转载 2025年12月11日 Augment Code Review在唯一的AI辅助代码审查公共基准测试中取得了最高的准确度,在整体质量上比Cursor Bugbot、CodeRabbit等其他系统高出约10个百分点。一个关键原因是什么?我们选择GPT-5.2作为代码审查的基础模型——以及我们的模型无关方法让我们能够为软件开发生命周期的每个阶段选择最佳工具。Augment Code Review最初基于GPT-5构建,但随着我们观察到OpenAI最新推理模型的质量提升,我们升级到了5.2版本。


doc文件?【图文详解】docx文件?xls/xlsx/ppt/pptx/pdf等办公文件怎么打开?
极智-9962025/12/3

一、问题背景         有时候电脑里蹦出个 “XX.docx” 文件,想打开却懵圈 —— 这后缀名跟 “XX.doc” 就差个 x,到底有啥不一样?存表格时纠结存成 “xls” 还是 “xlsx”,怕选错了下次打不开;看到 “ppt” 和 “pptx” 更是犯嘀咕,明明都是演示文稿,为啥名字尾巴不一样?还有 PDF,明明跟 Word 都能存文字,却死活改不了内容,这又是为啥?         其实啊,这些长得像 “小尾巴” 的后缀名,就是办公文件的 “身份证”!咱们每天用电脑处理工作、


三分钟说清楚 ReAct Agent 的技术实现
indieAI2026/1/15

ReAct Agent 技术实现主要依赖于精心设计的 Prompt 模板、输出解析器和执行循环三大核心机制。 1. 核心 Prompt 工程 LangChain 使用特定的 Prompt 模板引导 LLM 按 Thought → Action → Observation 格式输出: # 简化的 Prompt 结构 template = """ 用以下工具回答问题: 工具: - search: 搜索引擎, 输入: "查询词" - calculator: 计算器, 输入: "算式" 现在开始


多网卡如何区分路由,使用宽松模式测试网络
venus602026/1/23

一、什么是 Linux 的“非对称路由” 1️⃣ 定义(先给结论) 非对称路由指的是: 数据包从 A 网卡进来,但回包却从 B 网卡出去 在多网卡、多出口服务器上非常常见,比如: 双网卡 多默认网关 同一台服务器连多个网络 你之前的情况就是典型的非对称路由。 2️⃣ Linux 默认为什么不喜欢非对称路由? 因为它可能意味着: IP 欺骗(spoofing) 流量劫持 路由异常 所以 Linux 默认启用了一个安全机制: 👉

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客