我构建了一个永不看到你的图像的图像压缩器
Source: Dev.to
请提供您希望翻译的具体文本内容(除代码块和 URL 之外),我将为您翻译成简体中文并保持原有的格式、Markdown 语法以及技术术语不变。
MiniPx – 完全基于浏览器的图像压缩器
我尝试的每个在线图像压缩器都有同样的问题:它们会把你的照片上传到服务器。
TinyPNG、iLoveIMG、Compress2Go —— 它们的工作方式都一样。你挑选一个文件,它会被发送到别人的电脑上进行压缩,然后再返回。压缩效果不错,但照片(其中包含 GPS 坐标、设备序列号和时间戳等 EXIF 数据)会存放在你无法控制的服务器上。
我一直在想:图像压缩只是数学——Canvas API、质量参数和 blob 操作。没有理由必须使用服务器。
所以我构建了 MiniPx
它在 浏览器内部 完成图像的压缩、转换和尺寸调整。永远不会上传任何内容。下面是它的工作原理。
核心压缩循环
实际的压缩只需大约 20 行代码。将图像加载到 canvas 中,绘制它,然后使用质量参数将其导出为 blob:
function compressAtQuality(img, w, h, fmt, quality) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
// White background for JPEG (no transparency support)
if (fmt === 'image/jpeg') {
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, w, h);
}
ctx.drawImage(img, 0, 0, w, h);
canvas.toBlob(
(blob) => (blob ? resolve(blob) : reject(new Error('No output'))),
fmt,
fmt === 'image/png' ? undefined : quality
);
});
}
就是这么简单。无需 Sharp、ImageMagick,也不需要服务器端的任何东西。浏览器内置的 JPEG/WebP 编码器负责实际的压缩。
没有人谈论的问题:压缩导致文件变大
如果你拿一张经过良好优化的 JPEG 并在 Canvas 上使用 quality = 0.65 进行处理,输出的文件可能会 比输入的更大。浏览器会从头重新编码整张图片——它并不知道原始文件已经被压缩过。
在测试时,用户会上传一个 200 KB 的 JPEG,却得到一个 280 KB 的文件。这非常尴尬。
回退链
如果初始压缩产生了更大的文件,就逐步降低质量等级,直到压缩后文件小于原始文件:
let blob = await compressAtQuality(img, w, h, fmt, quality);
if (blob.size >= file.size && fmt !== 'image/png') {
for (const fallbackQ of [0.6, 0.45, 0.3, 0.2]) {
if (fallbackQ >= quality) continue;
const attempt = await compressAtQuality(img, w, h, fmt, fallbackQ);
if (attempt.size = file.size && fmt === 'image/webp') {
const jpegFallback = await compressAtQuality(
img,
w,
h,
'image/jpeg',
Math.min(quality, 0.5)
);
if (jpegFallback.size file.size * 1.5 && fmt === 'image/png') {
const webpAlt = await compressAtQuality(img, w, h, 'image/webp', quality);
const jpegAlt = await compressAtQuality(img, w, h, 'image/jpeg', quality);
const smallest = [blob, webpAlt, jpegAlt].sort((a, b) => a.size - b.size)[0];
if (smallest.size {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
img.src = 'data:image/heic;base64,AAAAGGZ0eXBoZWlj';
setTimeout(() => resolve(false), 500);
});
};
}
}
}
}
- Safari 用户 通过相同的 Canvas 技巧实现零依赖的 HEIC 转换:加载 HEIC,绘制到 Canvas,再导出为 JPEG。无需任何库。
- Chrome/Firefox 用户 使用
heic2any,这是一款基于 WASM 的 HEIC 解码器(约 350 KB)。它仅在实际需要转换 HEIC 文件时才懒加载:
const heic2any = (await import('heic2any')).default;
return await heic2any({ blob: file, toType: 'image/jpeg', quality: 0.92 });
Safari 永远不会下载这 350 KB;Chrome 用户只有在真的需要 HEIC 转换时才会下载。其他所有用户都走轻量化路径。
去除 EXIF 数据(隐私部分)
手机照片包含 EXIF 元数据:GPS 坐标、设备型号、序列号、时间戳,有时甚至还有你的姓名。
当你通过 Canvas 重新绘制图像时,EXIF 数据 不会随之带入。Canvas 只看到像素——它没有元数据的概念。因此,所有通过 MiniPx 处理的图像都会是干净的:没有 GPS、没有设备信息、没有时间戳。
提供了一个切换开关(“Strip EXIF data”),但默认 开启。Canvas 重新编码会自动处理——无需额外代码。
架构
MiniPx 是一个 Next.js 15 静态站点。它 没有 API 路由,也 没有服务器端处理;所有内容都在客户端浏览器中运行。
MiniPx 演示了现代浏览器已经具备安全、私密且高效的图像压缩、转换以及隐私保护的元数据剥离所需的一切——无需服务器。
概览
- 框架: Next.js 15(静态导出)
- 托管: Netlify(免费层)— 预渲染的 HTML + JS 通过 Netlify 的 CDN 提供
- 组件:
- 5 个客户端组件:
ImageTool、PDFTool、HEICTool、TrackedCTA、WebVitals - 其他所有部分均为服务器渲染(SEO 内容、模式、导航)
- 5 个客户端组件:
- 依赖: 共 8 个
性能
- 首次加载任意页面的 JavaScript:≈ 103‑106 KB(整个应用——React、压缩器、UI 等)
- 对比: TinyPNG 的主页加载 2.4 MB 的 JavaScript
我坚持尽可能使用服务器渲染。工具页面包含长篇 SEO 内容、FAQ 手风琴以及 JSON‑LD 模式,全部渲染为静态 HTML。唯一的客户端 JavaScript 是实际的图像处理工具。
我会做的不同之处
-
批处理速度
- 目前文件是顺序处理的。
- Web Workers 可以实现并行压缩,但 Canvas API 在 workers 中不可用。
OffscreenCanvas已存在,但浏览器支持仍然零星。我正在关注此事。
-
PNG 优化
- 客户端 PNG 优化仍是一个难题。
pngquant和oxipng的 WASM 移植版已存在,但会让 bundle 增加 500 KB+。- 目前,使用格式切换的回退方案可行,但本质上是个 hack。
-
预览功能
- 在下载前没有压缩后图像的预览。
- 添加并排预览可以提升用户体验,但这需要为每张图像保留两个 blob URL,在批量上传时会消耗大量内存。
试一试
MiniPx 免费。无需注册,无限制,无广告。
如果你正在构建类似的东西,关键洞见是:Canvas +
toBlob能提供约 90 % 的服务器端图像处理功能,且无需任何基础设施成本。
剩余约 10 %(PNG 优化、非 Safari 上的 HEIC、先进滤镜)需要 WASM 库,但你可以延迟加载这些库,从而让大多数用户根本不付出代价。