烦恼图片压缩📦
· 阅读需 5 分钟
看看我的画作页面,每次我打包我美好的作品,然后批量压缩的时候,我都挺可惜的。
压缩过的图片会很不精神,细节难免丢失。
如果没看过原图,那确实没什么关系,但是看过原图对比起来,诸如锐度下降,纯度下降,微妙的透明色丢失,尺寸变小都让人沮丧。
可是为什么必须压缩呢? 因为每张图片20多mb那就根本没法读取了(我只用得起3m小水管)。对于图像艺术家真的很苦恼☹️
所以目前方法是尽量用脚本批量成webp,把尺寸锁在2k(2560x1440),质量92%。别人不知道,起码自己观感好很多,读取速度也还能接受。(或者我未来能有效优化吧)
我目前用的压缩jsx是这个👇
路径是: /my-website/scripts/compress-images.js
compress-images.js
const fs = require('fs-extra');
const path = require('path');
const sharp = require('sharp');
// ============ 配置 ============
const SKETCH_DIR = path.join(__dirname, '../static/sketch');
const BACKUP_DIR = path.join(__dirname, '../static/sketch-backup');
const MAX_WIDTH = 2560; // 最大宽度 2560px
const MAX_HEIGHT = 1440; // 最大高度 1440px
const WEBP_QUALITY = 92; // WebP 质量 92%
const WEBP_EFFORT = 6; // 压缩努力程度 0-6
// ==============================
// 支持的图片格式(会被转成 WebP)
const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp', '.avif'];
// 获取文件大小
function getFileSize(filePath) {
const stats = fs.statSync(filePath);
return stats.size;
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + 'B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB';
return (bytes / (1024 * 1024)).toFixed(2) + 'MB';
}
// 处理单张图片 - 转 WebP
async function processImage(inputPath, backupPath) {
// 备份(如果还没备份过)
if (!fs.existsSync(backupPath)) {
fs.ensureDirSync(path.dirname(backupPath));
fs.copyFileSync(inputPath, backupPath);
console.log(`📦 已备份: ${path.relative(process.cwd(), inputPath)}`);
}
const originalSize = getFileSize(inputPath);
const metadata = await sharp(inputPath).metadata();
const ext = path.extname(inputPath).toLowerCase();
const fileName = path.basename(inputPath, ext);
const dir = path.dirname(inputPath);
// 构建 WebP 输出路径
const webpPath = path.join(dir, fileName + '.webp');
// 开始处理
let pipeline = sharp(inputPath);
// 缩放(如果宽度或高度超标)
if (metadata.width > MAX_WIDTH || metadata.height > MAX_HEIGHT) {
pipeline = pipeline.resize({
width: MAX_WIDTH,
height: MAX_HEIGHT,
fit: 'inside',
kernel: 'lanczos3',
withoutEnlargement: true,
});
}
// 转换为 WebP
pipeline = pipeline.webp({
quality: WEBP_QUALITY,
alphaQuality: WEBP_QUALITY, // 透明通道质量
lossless: false,
effort: WEBP_EFFORT,
nearLossless: false,
smartSubsample: true, // 智能子采样
});
// 写入 WebP 文件
await pipeline.toFile(webpPath);
// 删除原图(如果不是 WebP)
if (ext !== '.webp') {
fs.unlinkSync(inputPath);
} else {
// 如果是 WebP,删除临时文件(实际上就是原文件)
// 但我们已经覆盖了,所以不用额外操作
}
const newSize = getFileSize(webpPath);
const saved = ((originalSize - newSize) / originalSize * 100).toFixed(1);
const ratio = (newSize / originalSize * 100).toFixed(1);
const fromExt = ext !== '.webp' ? ext.toUpperCase().replace('.', '') : 'WebP';
console.log(`🔄 ${fromExt} → WebP: ${path.basename(inputPath)}`);
console.log(` 📊 ${formatSize(originalSize)} → ${formatSize(newSize)} (${ratio}%,节省 ${saved}%)`);
return { webpPath, originalPath: inputPath };
}
// 递归扫描文件夹
async function scanDirectory(dir, backupBaseDir) {
const items = fs.readdirSync(dir);
let processed = 0;
let totalFiles = 0;
let skipped = 0;
let totalSaved = 0;
let totalOriginal = 0;
let converted = 0;
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
// 跳过备份目录
if (fullPath === BACKUP_DIR) continue;
const subBackupDir = path.join(backupBaseDir, item);
const result = await scanDirectory(fullPath, subBackupDir);
processed += result.processed;
totalFiles += result.totalFiles;
skipped += result.skipped;
totalSaved += result.totalSaved || 0;
totalOriginal += result.totalOriginal || 0;
converted += result.converted || 0;
} else {
const ext = path.extname(item).toLowerCase();
if (IMAGE_EXTS.includes(ext)) {
totalFiles++;
try {
const relativePath = path.relative(SKETCH_DIR, fullPath);
const backupPath = path.join(BACKUP_DIR, relativePath);
// 如果是 GIF,跳过
if (ext === '.gif') {
console.log(`⏭️ 跳过 GIF(保留动画): ${path.basename(fullPath)}`);
skipped++;
continue;
}
const originalSize = getFileSize(fullPath);
await processImage(fullPath, backupPath);
// 获取新文件大小
const fileName = path.basename(fullPath, ext);
const dir = path.dirname(fullPath);
const webpPath = path.join(dir, fileName + '.webp');
const newSize = getFileSize(webpPath);
totalSaved += (originalSize - newSize);
totalOriginal += originalSize;
converted++;
processed++;
} catch (err) {
console.error(`❌ 处理失败 ${item}:`, err.message);
}
} else {
skipped++;
}
}
}
return { processed, totalFiles, skipped, totalSaved, totalOriginal, converted };
}
// ============ 主程序 ============
async function main() {
console.log('🚀 开始批量转换 WebP...\n');
console.log(`📊 最大尺寸: ${MAX_WIDTH}×${MAX_HEIGHT}px`);
console.log(`📊 WebP 质量: ${WEBP_QUALITY}%`);
console.log(`📊 压缩努力度: ${WEBP_EFFORT}/6\n`);
if (!fs.existsSync(SKETCH_DIR)) {
console.error('❌ 找不到 static/sketch 目录');
process.exit(1);
}
console.log('📊 正在扫描...');
const result = await scanDirectory(SKETCH_DIR, BACKUP_DIR);
const totalMB = (result.totalOriginal / (1024 * 1024)).toFixed(1);
const savedMB = (result.totalSaved / (1024 * 1024)).toFixed(1);
const finalMB = ((result.totalOriginal - result.totalSaved) / (1024 * 1024)).toFixed(1);
const savedPercent = result.totalOriginal > 0 ? (result.totalSaved / result.totalOriginal * 100).toFixed(1) : 0;
console.log('\n' + '='.repeat(50));
console.log(`✅ 全部完成!`);
console.log(` 📁 转换了 ${result.converted} 张图片为 WebP`);
console.log(` 📁 处理了 ${result.processed} 张图片`);
console.log(` 📁 共 ${result.totalFiles} 张图片(跳过 ${result.skipped} 张)`);
console.log(` 📊 总大小: ${totalMB}MB → ${finalMB}MB`);
console.log(` 📊 节省: ${savedMB}MB (${savedPercent}%)`);
console.log(` 📁 原图备份在: ${BACKUP_DIR}`);
console.log(`\n💡 所有图片已转换为 .webp 格式!`);
console.log(`💡 如果效果不满意,可以从备份恢复:`);
console.log(` cp -r ${BACKUP_DIR}/* ${SKETCH_DIR}/`);
}
main().catch((err) => {
console.error('❌ 程序出错:', err.message);
process.exit(1);
});
在根目录运行以下终端指令,他会生成一个备份文件夹📂,你也可以做成bat或者其他脚本。
npm run compress
以下听不懂,直接把上面的脚本扔给ai改
📌 使用前注意 依赖:fs-extra 和 sharp,运行前先装:
npm install fs-extra sharp
图片默认放在 static/sketch/ 下,如果你的图片在其他地方,修改脚本开头的 SKETCH_DIR 和 BACKUP_DIR 就行。 在 package.json 的 scripts 里加一行:
"compress": "node scripts/compress-images.js"
然后 npm run compress 就能跑了(大概)。