import sharp from 'sharp'; import { readdir, stat, rename } from 'node:fs/promises'; import { join, extname, basename } from 'node:path'; const MAX_WIDTH = 1600; const MIN_BYTES_TO_OPTIMISE = 250 * 1024; const dirs = ['src/lib/images', 'static/images']; async function optimiseFile(file) { const ext = extname(file).toLowerCase(); const input = await sharp(file, { failOn: 'none' }).rotate(); const meta = await input.metadata(); const width = meta.width ?? 0; const targetWidth = width > MAX_WIDTH ? MAX_WIDTH : width; const pipeline = sharp(file, { failOn: 'none' }) .rotate() .resize({ width: targetWidth, withoutEnlargement: true }); let buf; if (ext === '.png') { if (meta.hasAlpha) { buf = await pipeline .png({ palette: true, quality: 88, compressionLevel: 9, effort: 10 }) .toBuffer(); } else { buf = await pipeline .png({ palette: true, quality: 82, compressionLevel: 9, effort: 10 }) .toBuffer(); } } else if (ext === '.jpg' || ext === '.jpeg') { buf = await pipeline.jpeg({ quality: 82, mozjpeg: true }).toBuffer(); } else if (ext === '.webp') { buf = await pipeline.webp({ quality: 80, effort: 6 }).toBuffer(); } else { return null; } const original = (await stat(file)).size; if (buf.length >= original) return { file, original, optimised: original, skipped: true }; const tmp = file + '.opt.tmp'; await sharp(buf).toFile(tmp); await rename(tmp, file); return { file, original, optimised: buf.length, skipped: false }; } let totalOrig = 0; let totalNew = 0; for (const dir of dirs) { const entries = await readdir(dir); for (const name of entries) { if (!/\.(png|jpe?g|webp)$/i.test(name)) continue; const file = join(dir, name); const s = await stat(file); if (s.size < MIN_BYTES_TO_OPTIMISE) continue; try { const res = await optimiseFile(file); if (!res) continue; totalOrig += res.original; totalNew += res.optimised; const pct = ((1 - res.optimised / res.original) * 100).toFixed(0); const flag = res.skipped ? ' (skipped: no gain)' : ''; console.log( `${basename(file).padEnd(58)} ${(res.original / 1024).toFixed(0).padStart(5)}KB → ${(res.optimised / 1024).toFixed(0).padStart(5)}KB (-${pct}%)${flag}` ); } catch (err) { console.error('FAILED', file, err.message); } } } console.log(`\nTotal: ${(totalOrig / 1024 / 1024).toFixed(2)} MB → ${(totalNew / 1024 / 1024).toFixed(2)} MB`);