最佳阅读体验在 https://blog.cosine.ren/post/server-batch-image-compression
# 前言
最近需要处理别人的服务器上大量历史图片的压缩问题。因为历史遗留原因, public/uploads 下累积了不少高分辨率的全景图和常规照片,占用存储空间越来越大。虽然之前在前端生产环境中一直使用 sharp-cli、@napi-rs/image 等进行图片处理,但这次面对的是已存储的历史图片批量压缩场景,而且这次提供的环境是堡垒机,只能通过网页终端进行操作,没有 GUI,需要更灵活的命令行工具。
这篇文章记录了完整过程,希望对有类似需求的朋友有所帮助。
# 为什么不用 sharp-cli?
sharp-cli 和 @napi-rs/image 一般是我在生产环境中的首选,性能优异且 API 友好。但这次需求是批量处理已存储的大量图片,而 sharp-cli 更适合集成到构建流程或 Node.js 脚本中。对于这种一次性批量操作,传统的 ImageMagick/libvips 命令行工具更加直接。
# 工具对比
在压缩之前我看过了这些工具:
| 工具 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| ImageMagick | 功能全面,文档丰富 | 大图处理内存占用高 | 通用图片处理 |
| libvips | 流式处理,内存效率极高 | 功能相对聚焦 | 超大图片 / 批量处理 |
| sharp-cli | 性能优异,API 友好 | 需要 Node.js 环境 | 构建流程集成 |
| Squoosh CLI | 支持现代格式 (WebP/AVIF) | 已停止维护 | ❌ 不推荐生产使用 |
| guetzli/mozjp | 极致压缩 (JPEG) | 处理速度极慢 | 追求极限压缩率场景 |
最终选择:ImageMagick + libvips 组合方案
- 一开始使用 ImageMagick 处理 10MB 以下的常规图片,很快速很方便,但发现超大图片(100MB+)会爆内存。
- libvips 是 sharp 底层使用的库,处理超大全景图(100MB+)绰绰有余。
infographic compare-hierarchy-left-right-circle-node-pill-badge
data
title 工具选择策略
items
- label ImageMagick
children
- label 常规图片
desc <10MB 的图片
- label 功能全面
desc 支持各种格式转换
- label 语法简单
desc mogrify 一行命令
- label libvips
children
- label 超大图片
desc 100MB+ 全景图
- label 内存效率
desc 流式处理不爆内存
- label sharp底层
desc 生产级性能保证
# 开始前准备
infographic list-row-simple-horizontal-arrow
data
title 操作流程
items
- label 检查容量
desc 查看磁盘剩余空间
- label 统计大小
desc 评估压缩前文件大小
- label 备份数据
desc 防止误操作
- label 执行压缩
desc 使用合适工具
最重要的两点当然是:
- 查看原来的所有图片大小
- 备份你要压缩的图片(因为 mogrify 会在原图的基础上直接进行压缩,所以备份很重要)
# du(disk usage) | |
# -sh: 只显示每个参数的总大小,以人类可读的格式显示 (K, M, G) | |
# sort -hr: 按大小倒序排列 | |
du -sh * | sort -hr | |
# 只列出 17 开头的 | |
du -sh 17* | sort -hr |
输出如下:
root@3d:/myapp/storage/uploads/panorama/backup# du -sh * | sort -hr | |
147M 1766019200667-1c2d8x.jpg | |
130M 1766019053272-aylt0b.jpg | |
120M 1766019107903-jl2i2s.jpg | |
63M 1765977415640-hu7yap.jpg | |
…… |
然后备份,备份前记得 df -h 查看磁盘剩余可用空间。
df -h | |
# 可用空间足够,则进行备份 | |
tar -cvf uploads_backup_$(date +%Y%m%d).tar uploads/ | |
# 或者比如说我要备份 17 开头的 | |
tar -czvf backup_17_$(date +%Y%m%d).tar.gz 17* | |
# 备份所有 17 开头且大于 10M 的文件 | |
tar -czvf backup_17_large_$(date +%Y%m%d).tar.gz $(find . -name "17*" -size +10M) |
会把 upload 目录备份为 uploads_backup_20251223.tar 文件
# 工具安装
然后就到了开始实行压缩的环节
# ImageMagick
https://imagemagick.org/script/command-line-processing.php
# Debian/Ubuntu | |
sudo apt update && sudo apt install imagemagick -y | |
# macOS | |
brew install imagemagick | |
# 验证 | |
magick -version |
需要注意的是新版 ImageMagick 7+ 推荐使用 magick 命令,但是服务器上比较老所以我用的旧语法。
# libvips
https://www.libvips.org/API/8.17/using-vipsthumbnail.html
# Debian/Ubuntu | |
sudo apt install libvips-tools -y | |
# macOS | |
brew install vips | |
# 验证 | |
vipsthumbnail --version |
# 踩坑记录
# 超大图片内存溢出
处理 150MB 的全景图时遭遇 cache resources exhausted 错误:
# 假设以 change 开头的是我们要批量压缩的图片 | |
# 原图可能有 20000x 以上,可以 resize 到 9000x 以下 | |
mogrify -quality 75 -resize "9000x>" ./change*.jpg |
根本原因:
ImageMagick 采用全图加载到内存的处理方式:
对于 150MB 的 JPEG, 解压后像素数据可达 900MB-2GB, 超出了默认资源限制。
ImageMagick 处理超大图很吃力,此时可以请出 libvips。
# 1. 先看看有哪些文件 | |
find . -maxdepth 1 -name "17*.jpg" -size +10M -exec ls -lh {} \; | |
# 2. 备份 (安全起见) | |
tar -czvf backup_17_10M_$(date +%Y%m%d).tar.gz 17*.jpg | |
# 3. 执行压缩 | |
for f in 17*.jpg; do | |
size=$(stat -c%s "$f" 2>/dev/null) | |
if [ $size -gt 10485760 ]; then | |
echo "Compressing: $f ($(du -h "$f" | cut -f1))" | |
vipsthumbnail "$f" -s 9000 -o "temp_[Q=75].jpg" | |
if [ -f "temp_.jpg" ]; then | |
mv "temp_.jpg" "$f" | |
echo "Done: $f → $(du -h "$f" | cut -f1)" | |
fi | |
fi | |
done | |
# 4. 检查结果 | |
du -sh 17*.jpg | sort -hr |
注意这里 temp_[Q=75].jpg 中的 [Q=75] 会被替换为空,这是 vipsthumbnail 的文件名替换规则。
在 vipsthumbnail 命令中,-o 参数支持特殊的占位符替换。而 [Q=75] 是一个保存选项占位符,用于指定 JPEG 质量
在最终生成的文件名中,这部分会被移除(替换为空)

大图片压缩圆满完成。
# 分目录 mogrify 批量压缩
注意 mogrify 会在原图的基础上直接进行压缩,所以备份很重要
较小的图片使用 ImageMagick 的 mogrify 进行就地批量修改:
# 先备份 | |
cp -r . ../backup_before_mogrify/ | |
# 就地修改(mogrify) | |
mogrify -quality 75 -resize "4000x>" input.jpg | |
# 或输出到新文件(convert) | |
convert input.jpg -quality 75 -resize "4000x>" output.jpg |
其他常用的 resize 选项:
"4000x>" # 只缩小,宽度限制 4000px | |
"4000x4000>" # 只缩小,宽高都不超过 4000px | |
"50%" # 缩小到 50% | |
"4000x3000!" # 强制缩放到指定尺寸 (可能变形) |
有很多的子目录
root@3d:/myapp/storage/uploads/panorama# du -sh * | sort -hr | |
131M cmjpqjihk002mlm01qyyk3vqc | |
131M cmjpp9pfk001ulm01dr2aina9 | |
129M cmjxtmsfs00eilm01iso8kaq2 | |
128M cmja74wwh00fcl701ie777fnj | |
127M cmja751nb00fel701u4oqmgfk | |
126M cmk0yw2zx004nlp01i7r22uoe |
里面是这种格式的相对小的图片。
root@3d:/myapp/storage/uploads/panorama/cmjpqjihk002mlm01qyyk3vqc# du -sh * | sort -hr | |
26M cmjpqjihk002mlm01qyyk3vqc.right.jpg | |
26M cmjpqjihk002mlm01qyyk3vqc.left.jpg | |
25M cmjpqjihk002mlm01qyyk3vqc.back.jpg | |
24M cmjpqjihk002mlm01qyyk3vqc.front.jpg | |
18M cmjpqjihk002mlm01qyyk3vqc.bottom.jpg | |
13M cmjpqjihk002mlm01qyyk3vqc.top.jpg |
那我就直接用 mogrify -quality 75 -resize "4000x>" ./public/uploads/**/*.jpg 了,快一些。
我需要压缩大于 20M 的目录,并且批量先 cd 进目录,处理完再 cd .. ,过程中我还想看进度:
先备份大于 20M 的目录
backup_file="../panorama_backup_$(date +%Y%m%d_%H%M%S).tar.gz" | |
dirs_to_backup="" | |
for dir in */; do | |
size=$(du -sm "$dir" | cut -f1) | |
if [ $size -gt 20 ]; then | |
echo "加入备份: $dir ($size MB)" | |
dirs_to_backup="$dirs_to_backup $dir" | |
fi | |
done | |
tar -czf "$backup_file" $dirs_to_backup | |
echo "备份完成: $backup_file" |
备忘录
du -sm中的 -m 参数指定了单位是 MB(megabytes)- -s 是 summarize(汇总)
- -m 是 megabytes(兆字节)
cut -f1提取第一列(数字部分)tar -czf创建一个用 gzip 压缩的 tar 归档文件,名字叫 backup.tar.gz- -c = create 创建新的归档文件
- -z = gzip 使用 gzip 压缩
- -f = file 指定文件
开始压缩,使用 mogrify -quality 75 -resize "4000x>" "$img" (ImageMagick 7+ 推荐使用 magick mogrify 代替旧版的 mogrify )
使用 ImageMagick 压缩和调整图片
mogrify直接修改原图片(不创建副本)-quality 75设置 JPEG 质量为 75%-resize "4000x>"如果宽度超过 4000 像素就缩小,保持宽高比,>表示只缩小不放大
以下命令的大意是:
- 遍历所有以 cmj 开头的子目录,检查每个目录的大小
- 筛选大于 20 MB 的目录进行处理(跳过已经压缩过的小目录)
- 对符合条件的目录:进入目录 -> 统计其中的 JPG 文件数量 -> 逐个处理每张 JPG 图片 -> 降低质量到 75%,如果宽度超过 4000 像素则缩小到 4000 像素(保持宽高比)
- 显示实时进度(如 [3/10] photo.jpg)
- 处理完成后返回上级目录,继续处理下一个目录
for dir in cmj*/; do | |
size=$(du -sm "$dir" | cut -f1) | |
if [ $size -gt 20 ]; then | |
echo "处理: $dir ($size MB)" | |
cd "$dir" | |
total=$(ls *.jpg 2>/dev/null | wc -l) | |
count=0 | |
for img in *.jpg; do | |
((count++)) | |
echo " [$count/$total] $img" | |
mogrify -quality 75 -resize "4000x>" "$img" | |
done | |
cd .. | |
echo "完成: $dir" | |
fi | |
done |
备忘录
wc -l中的-l是 lines(行数),统计行数((count++))是 bash 的算术运算语法,让变量自增 1mogrify是 ImageMagick 工具,直接修改原文件而不创建副本-quality 75设置 JPEG 压缩质量为 75%(0-100,数值越高质量越好文件越大)-resize "4000x>"中的>表示 "只缩小不放大",如果图片宽度 ≤4000 则不处理

效果拔群。
# 写在最后
# 为什么不直接用 sharp-cli?
sharp 非常优秀,我在生产环境的图片上传流程中一直使用。但对于已存储的历史图片批量压缩,传统命令行工具更灵活:
- 不需要写 Node.js 脚本
- 直接用 shell 管道 /find 组合
- libvips 本身就是 sharp 的底层引擎
- 下次再用直接改改就可以
# 如何达到最优的压缩效果?
更细致的压缩质量的话,可以看这篇 Stack Overflow 帖子,很具体地讨论了使用 ImageMagick 压缩 JPEG 图片文件以实现最小文件大小但又不损失过多质量的问题。
https://stackoverflow.com/questions/7261855/best-options-parameters-for-imagemagick-to-compress-jpegs-files-to-the-minimal-s
以下是 AI 辅助总结的该帖子的要点总结
# 问题
原始提问者(Javis Perez)遇到的问题是:
- 压缩效果不佳: 使用 ImageMagick 压缩 JPEG 文件时,无法获得显著的文件大小差异。
- 输出文件反而更大: 在默认情况下,输出的图片文件大小甚至比输入文件更大。
- 尝试后仍不理想: 即使使用了
+profile "*"和-quality 70选项,输出文件(264KB)仍然接近或略大于输入文件(255KB)。 - 明确的目标: 希望能将图片压缩到至少 150KB,并寻求实现这一目标的 ImageMagick 选项。
# 解决方式
帖子中提供了多种解决方案和参数组合,可以归纳为以下几点:
核心参数组合 (最受推荐和高赞的方案):
- Command:
convert -strip -interlace Plane -gaussian-blur 0.05 -quality 85% source.jpg result.jpg - 参数解释:
-quality 85%: 设置 JPEG 压缩质量为 85%(可根据需求调整,通常在 60-85 之间,数字越低文件越小,但质量损失越大)。-strip: 移除图片中的所有元数据(如 EXIF 信息、评论等),这是无损缩小文件大小的有效方法。-interlace Plane(或-interlace JPEG): 创建渐进式 JPEG,虽然不直接减小文件大小,但能改善图片在网络上的加载体验。-gaussian-blur 0.05: 应用一个非常小的(如 0.05)高斯模糊。这个参数有争议,一些用户认为它能平滑图像中的噪点,从而提高压缩率,显著减小文件大小;另一些用户则认为它会造成图像模糊,不如直接降低-quality。
- Command:
Google PageSpeed Insights 推荐的优化方案:
- Command:
convert image.jpg -sampling-factor 4:2:0 -strip -quality 85 -interlace JPEG -colorspace RGB image_converted.jpg - 参数解释:
-sampling-factor 4:2:0: 这是色彩子采样,将色度通道的采样率减半,同时不影响亮度通道。人眼对亮度变化比色度变化更敏感,因此这种方法能在不明显影响视觉质量的情况下大幅度减小文件大小。-strip和-quality 85及-interlace JPEG: 同上。-colorspace RGB: 确保图片以正确的 RGB 色彩空间处理。
- 补充选项:
-define jpeg:dct-method=float: 使用更精确的浮点离散余弦变换(DCT)而不是默认的快速整数版本,可以在不增加文件大小的情况下略微提高转换的保真度。
- Command:
直接指定目标文件大小:
- Command:
convert image.jpg -define jpeg:extent=150kb result.jpg - 参数解释:
-define jpeg:extent={size}: 从 ImageMagick v6.5.8-2 开始,可以直接指定 JPEG 输出文件的最大大小(例如 "150kb")。ImageMagick 会自动调整质量以达到这个目标大小。这是解决原始问题中 "压缩到 150kb" 最直接的方法,但会带来质量损失。
- Command:
图像尺寸调整(最显著的减小文件大小的方法):
- Command (示例):
mogrify -quality "97%" -resize 2048x2048 -filter Lanczos -interlace Plane -gaussian-blur 0.05 - 参数解释:
-resize WxH或-adaptive-resize X%: 调整图像的尺寸。这是减小文件大小最有效的方法之一,尤其是对于高分辨率图片。-filter Lanczos: 一种高质量的缩放滤镜(通常是默认值)。
- 注意: 减小图片尺寸会改变图片的实际像素数据。
- Command (示例):
其他考量及建议:
- 批处理: 对于多个文件,可以使用
mogrify命令替代convert。例如:mogrify -strip -interlace Plane -gaussian-blur 0.05 -quality 85% *.jpg。注意:mogrify会覆盖原文件,操作前务必备份。 - 质量与模糊的权衡: 帖子中多次提及
-gaussian-blur的争议。许多用户认为简单地降低-quality即可达到目的,或者结合-sampling-factor 4:2:0更有效,避免引入不必要的模糊。对于大尺寸图片,微小的模糊可能不易察觉;对于小图片,则效果可能不佳。 - 外部工具: 推荐使用如 ImageOptim、pngquant 等外部工具进行进一步的无损或有损优化。
- 重新压缩的副作用: 有评论指出,对已压缩的 JPEG 进行再压缩,即使文件大小增大,也会导致图像质量下降。
- 批处理: 对于多个文件,可以使用
# Refs
- convert and mogrify: The correct way to use them in modern versions of ImageMagick
- Best options parameters for ImageMagick to compress JPEGs files to the minimal size without losing too much quality?
- ImageMagick Official Docs
- libvips Documentation
- sharp - High performance Node.js image processing
- cwebp - WebP encoder
本文随时修订中,有错漏可直接评论