用 Canvas 实现纯前端批量图片压缩工具

图片压缩并不一定需要后端服务。现代浏览器已经提供了文件读取、图片解码、Canvas 绘制和 Blob 编码能力,因此我们可以让图片从选择到下载始终留在用户设备中。

本文介绍本站图片压缩工具的实现思路。它支持批量选择图片、调整输出尺寸和质量、显示处理进度,以及预览和下载压缩结果。


一、为什么选择纯前端实现

纯前端压缩最明显的优势是隐私和响应速度:

  • 图片不上传服务器,适合处理包含隐私信息的照片
  • 不消耗服务器存储和带宽
  • 文件选择后即可开始处理
  • 即使部署在静态托管平台也能正常使用

代价是计算和内存均由用户设备承担。因此批量处理时需要控制并发数量,避免同时解码多张高分辨率图片。

二、处理流程

每张图片依次经过以下阶段:

选择文件 → 校验类型和大小 → 解码图片 → 计算输出尺寸
        → Canvas 绘制 → 编码 Blob → 创建预览地址 → 下载

工具接受 JPEG、PNG 和 WebP。GIF 没有加入支持列表,因为将 GIF 绘制到 Canvas 后只会保留其中一帧,动画会丢失。

三、多文件队列

文件选择框需要声明 multiple

<input
  type="file"
  accept="image/jpeg,image/png,image/webp"
  multiple
/>

每个队列项目除了保存原始 File,还需要记录状态、处理进度、预览地址、输出 Blob 和图片尺寸。使用独立状态后,一张图片失败不会阻止其他图片继续压缩。

工具使用文件名、大小和最后修改时间识别重复文件,并限制单张图片最大为 30 MB。

四、计算输出尺寸

为了避免图片变形,缩放时需要保持宽高比:

const scale = Math.min(
  1,
  maxWidth / sourceWidth,
  maxHeight / sourceHeight,
)

const width = Math.round(sourceWidth * scale)
const height = Math.round(sourceHeight * scale)

这里将 1 放入 Math.min,意味着小图片不会被放大。放大不仅不能增加真实细节,还可能产生更大的文件。

五、使用 Canvas 压缩

浏览器可以通过 createImageBitmap 解码图片,然后将其绘制到目标尺寸的 Canvas:

const bitmap = await createImageBitmap(file, {
  imageOrientation: "from-image",
})

const canvas = document.createElement("canvas")
canvas.width = width
canvas.height = height

const context = canvas.getContext("2d")
context?.drawImage(bitmap, 0, 0, width, height)
bitmap.close()

imageOrientation: "from-image" 会尽可能按照照片的方向信息解码。绘制完成后立即关闭 ImageBitmap,可以更早释放底层图像资源。

接着使用 canvas.toBlob() 编码输出:

canvas.toBlob((blob) => {
  if (!blob) throw new Error("Encode failed")
}, "image/webp", 0.8)

质量参数对 JPEG 和 WebP 最有效。PNG 是无损格式,不同浏览器通常会忽略它的质量参数;想明显缩小 PNG,可以降低分辨率或转换为 WebP。

六、压缩进度如何设计

Canvas 编码没有提供逐字节的进度事件,因此工具不会伪造一个精确的网络式进度。每个文件根据真实处理阶段更新:

  1. 等待处理
  2. 正在读取
  3. 正在解码
  4. 正在压缩
  5. 已完成或失败

总体进度是所有文件阶段进度的平均值,同时显示已完成数量。图片按顺序处理,以降低移动设备在处理大图时的内存压力。

七、预览和下载

Blob 本身不能直接作为图片地址,需要创建 Object URL:

const previewUrl = URL.createObjectURL(blob)

下载时创建一个临时链接并设置 download

const link = document.createElement("a")
link.href = previewUrl
link.download = "photo-compressed.webp"
link.click()

Object URL 会一直引用对应 Blob。删除图片或卸载组件时必须调用:

URL.revokeObjectURL(previewUrl)

否则用户反复添加和删除大图时,页面可能持续占用内存。

八、格式与隐私注意事项

Canvas 重新编码通常会移除 EXIF、GPS 和相机参数等元数据。这有助于减小文件,也能减少分享照片时意外暴露位置信息的风险,但需要保留元数据的摄影工作流并不适合这种方式。

另外,压缩并不保证输出一定更小。例如已经高度优化的图片转换为 PNG 后可能变大,所以结果列表会同时展示原始大小、输出大小和实际变化比例。

总结

借助 FilecreateImageBitmap、Canvas、Blob 和 Object URL,可以在不依赖后端和第三方压缩服务的情况下完成一套实用的图片优化流程。

实现时最重要的并不是调用一次 toBlob(),而是处理好批量任务、错误隔离、真实进度反馈、格式差异和内存释放。这些细节共同决定了工具在真实使用中的稳定性。