JavaScript 系列之:图片压缩
图像压缩原理
什么是有损压缩?
不会完全真实的记录图片信息,会根据人眼观察世界的特性,忽略掉部分会被人眼忽略的颜色信息,代之以邻近的颜色。解压后无法完全恢复原始图像,会损失一定质量。压缩率可通过参数调整,压缩率越高,图像质量损失越明显。
什么是无损压缩?
无损压缩会完整记录图片颜色信息,但是相同颜色的区域,会被压缩记录,因此无损压缩也可以比较完整的还原图片。压缩率通常较低。
格式 | 压缩方式 | 透明度支持 | 动画支持 | 兼容性 | 摘要 |
---|---|---|---|---|---|
JPEG | 有损 | 不支持 | 不支持 | 所有浏览器都兼容 | 静态图像有损压缩的理想选择(目前最流行) |
PNG | 无损 | 支持 | 不支持 | 除了 IE6 以外的所有浏览器都兼容 | 与 JPEG 相比,PNG 能更精确地再现源图像,或在需要透明的情况下更受青睐 |
GIF | 无损 | 支持 | 支持 | 所有浏览器都兼容 | 是简单图像和动画的不错选择 |
WebP | 有损和无损 | 支持 | 支持 | 兼容性较差,只有主流浏览器的较新版本才支持 | 是静止图像和动画图像的【绝佳选择】,WebP 的压缩效果比 PNG 或 JPEG 好得多 |
HIEF | 有损和无损 | 支持 | 支持 | 兼容性较差,主要在 Apple 生态系统 | 下一代高效图像格式,压缩效率显著优于 JPEG/PNG,节省大量存储空间。 |
前端压缩方法
使用 canvas
主流方法:
利用 Canvas 的绘图能力,使用 drawImage 以及 toDataURL 这两个 API,通过调整图片的尺寸或者绘图质量,来达到图片压缩的效果。
-
优点:实现简单,参数可配置化,可自定义图片尺寸,指定区域裁剪等等。
-
缺点:只有 jpeg 、webp 支持原图尺寸下图片质量的调整,来达到压缩图片的效果,其他图片格式仅能通过调整尺寸来实现。
实现过程:
-
使用 FileReader 和 Image 对象加载图片
-
将图片绘制到
<canvas>
上,通过调整<canvas>
的尺寸或质量来控制压缩效果 -
使用
canvas.toDataURL()
或canvas.toBlob()
方法导出新图片
核心代码
const compressImage = (file, options = {}, callback) => {const {maxSize = 100, // 默认最大100kbmaxWidth, // 无默认值,不传递则不限制maxHeight, // 无默认值,不传递则不限制quality = 0.9 // 默认质量0.9} = options;/*** 首先,我们需要创建一个Canvas元素,并将图像绘制到Canvas上。* 然后,我们可以调整Canvas的大小,以实现图像的压缩。*/const reader = new FileReader();reader.onload = (e) => {const img = new Image();img.onload = () => {const canvas = document.createElement('canvas');let width = img.width;let height = img.height;if (maxWidth && width > height) {if (width > maxWidth) {height *= maxWidth / width;width = maxWidth;}} else {if (maxHeight && height > maxHeight) {width *= maxHeight / height;height = maxHeight;}}canvas.width = width;canvas.height = height;const ctx = canvas.getContext('2d');ctx.drawImage(img, 0, 0, width, height);/*** HTMLCanvasElement.toBlob() 方法创造 Blob 对象,用以展示 canvas 上的图片;* toBlob(callback, type, quality)* callback* 回调函数,可获得一个单独的 Blob 对象参数。如果图像未被成功创建,可能会获得 null 值。* type 可选* DOMString 类型,指定图片格式,默认格式(未指定或不支持)为 image/png。* type 最好手动指定为 image/jpeg 格式,因为 png 不支持有损压缩,即不能通过设置质量来进行压缩,webp 兼容性差,所以 jpeg 是最合适的。* quality 可选* Number 类型,值在 0 与 1 之间,当请求图片格式为 image/jpeg 或者 image/webp 时用来指定图片展示质量。如果这个参数的值不在指定类型与范围之内,则使用默认值,其余参数将被忽略。* 返回值* 无*/canvas.toBlob(function (blob) {if (!blob) {console.error('压缩失败');callback(null);return;}if (blob.size / 1024 <= maxSize || quality <= 0.1) {callback(blob, file);} else {const newFile = new File([blob], file.name, {type: blob.type,lastModified: Date.now()});compressImage(newFile, { ...options, quality: quality - 0.1 }, callback);}}, 'image/jpeg', quality);},img.onerror = (e) => {console.log("图片加载失败:", e);}img.src = e.target.result;}reader.onerror = (e) => {console.log("文件读取失败:", e);}/*** readAsDataURL:将文件内容读取为 Base64 编码的 Data URL。* 适用场景:* 图片/文件的预览(如 <img src="data:image/png;base64,...">)* 需要将文件直接嵌入网页或上传到服务器(Base64 格式)。* * readAsText:将文件内容读取为 纯文本字符串。* 适用场景:* 读取 .txt、.csv、.json 等文本文件。* * readAsArrayBuffer:将文件读取为 ArrayBuffer(二进制数据缓冲区)。* 适用场景:* 处理二进制文件(如图片、音频、视频的原始字节)。*/reader.readAsDataURL(file);
}
初学者可能看的比较迷糊,这里来详细解释一下:
这种写法是 JavaScript 处理异步操作的典型模式:先定义好所有的处理规则和回调函数,最后才启动实际的异步操作(文件读取),确保操作完成时所有的处理函数都已准备就绪。简单来说就是“先准备后执行”。
代码解释:
// 创建一个文件读取器,用于读取文件内容。
const reader = new FileReader();
// 文件成功读取时触发
reader.onload = (e) => {// ... 函数体 ...
}
// 图片成功加载完成时触发
img.onload = () => {// ... 压缩处理的核心逻辑 ...
}
// 设置图片源,触发图片加载
img.src = e.target.result;
// 开始读取文件
reader.readAsDataURL(file);
所以上面这段代码的逻辑是:
-
首先定义了 reader.onload(文件读取成功的回调函数)
-
在 reader.onload 内部定义了 img.onload(图片加载成功的回调函数)
-
通过 reader.readAsDataURL(file) 开始读取文件
-
文件读取成功后执行 reader.onload
-
在 reader.onload 中通过 img.src = e.target.result 开始加载图片
-
图片加载成功后执行 img.onload
兼容性问题
这段代码在 PC 和 安卓手机上都能完美运行,能够极大压缩图片质量,但是在 IOS 设备上就差强人意,压缩效果不及安卓,甚至会出现越大越大的情况。
经查阅资料可能是以下两个原因造成的:
-
在 IOS 设备中会有 HIEF 格式的图片,而 HEIF 是一种比 JPEG 高效得多的压缩格式(相同画质下文件更小),所以当把 HEIF 转为 JPEG 时,就可能因为格式本身的压缩效率差异导致文件变大
-
安卓设备浏览器通常使用 Blink 内核,而 IOS 设备浏览器使用 WebKit 内核。(微信内置浏览器安卓设备是 X5 内核,基于 Blink 内核开发,与 Chrome 同源,IOS 设备也是 WebKit 内核)
-
Blink 可能会更激进地丢弃图像细节以减小体积,而 WebKit 可能保留更多细节
-
Blink 在低 quality 下可能压缩得更彻底,而 WebKit 对低 quality 值(如 0.1 以下)的处理更保守,即使设置极低的 quality,也不会无限制压缩(避免图像过度失真)。
-
针对 IOS 设备的解决方法:
-
缩小图片尺寸
-
动态调整 quality 步长,把每次减 0.1 改成每次减 0.3 或者更多
-
降低初始 quality 初始值,例如直接设置 quality 为 0.1。
我使用第三种方法直接将 quality 初始值设置为 0.1,经过实际测试,确实能有效降低压缩后图片大小。
完整代码示例:
<template><div><input type="file" id="file1" @change="fileChange" /><input type="file" id="file2" @change="(e) => fileChange(e, 0.5)" /><div>{{ loading }}</div></div>
</template><script setup>
import { onMounted, ref } from 'vue';
const loading = ref('')
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;onMounted(() => { })const fileChange = (e, quality) => {const file = e.target.files[0];const maxSize = 100;console.log("是IOS设备吗:", isIOS);if (file.size / 1024 <= maxSize) {alert(`图片大小${file.size / 1024}KB小于${maxSize}KB,无需压缩,直接下载`);const blob = new Blob([file], { type: file.type });createDownloadLink(blob, file);return;}alert(`图片原始大小:${(file.size / 1024).toFixed(2)}KB,${(file.size / 1024 / 1024).toFixed(2)}MB,类型${file.type}`)loading.value = '图片压缩中。。。'compressImage(file, { maxSize, quality: isIOS ? 0.1 : quality }, compressCallback);
}// 这里的file是为了获取原来的文件名用的
const compressCallback = (blob, file) => {alert(`图片压缩完毕后大小:${(blob.size / 1024).toFixed(2)}KB,${(blob.size / 1024 / 1024).toFixed(2)}MB`)createDownloadLink(blob, file)loading.value = '图片压缩完毕!'
}const compressImage = (file, options = {}, callback) => {const {maxSize = 100, // 默认最大100kbmaxWidth, // 无默认值,不传递则不限制maxHeight, // 无默认值,不传递则不限制quality = 0.9 // 默认质量0.9} = options;console.log("compressImage quality:", quality);const reader = new FileReader();reader.onload = (e) => {const img = new Image();img.onload = () => {const canvas = document.createElement('canvas');let width = img.width;let height = img.height;if (maxWidth && width > height) {if (width > maxWidth) {height *= maxWidth / width;width = maxWidth;}} else {if (maxHeight && height > maxHeight) {width *= maxHeight / height;height = maxHeight;}}canvas.width = width;canvas.height = height;const ctx = canvas.getContext('2d');ctx.drawImage(img, 0, 0, width, height);canvas.toBlob(function (blob) {if (!blob) {console.error('压缩失败');callback(null);return;}if (blob.size / 1024 <= maxSize || quality <= 0.1) {callback(blob, file);} else {const newFile = new File([blob], file.name, {type: blob.type,lastModified: Date.now()});compressImage(newFile, { ...options, quality: quality - 0.1 }, callback);}}, 'image/jpeg', quality);},img.onerror = (e) => {console.log("图片加载失败:", e);}img.src = e.target.result;}reader.onerror = (e) => {console.log("文件读取失败:", e);}reader.readAsDataURL(file);
}/*** 创建下载链接* @param blob * @param file */
const createDownloadLink = (blob, file) => {let url = window.URL.createObjectURL(blob);console.log("url:", url, typeof url);let a = document.createElement("a");a.href = url;a.download = file.name; // blob对象本身并不直接存储文件名document.body.appendChild(a)a.click();document.body.removeChild(a)window.URL.revokeObjectURL(url)
}</script><style scoped></style>
使用算法
简单来说,通过算法减少图片上的颜色差异,牺牲图片画质。比如紧挨着的颜色相近的四个像素的颜色信息压缩前大概占16个字节,压缩后变成一个颜色就能减少近4倍。自然被压缩后文件就变小,画质自然也会降低。
-
优点:色彩丰富场景压缩率更高,参数可配置化,可自定义图片的尺寸,图片的质量等。
-
缺点:图片质量压缩损失更大。
参考内容
前端性能优化系列 - 图像压缩篇