perf: convert all images to webp, update references
- 77 jpg/png/gif → webp (kept 7 where webp larger) - public/ assets: 23.3 MB → ~12 MB (~50% smaller) - 110 image references updated across data files + components - scripts/convert-to-webp.mjs + scripts/fix-image-refs.mjs added Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
de25d774af
commit
dda358b59d
@@ -0,0 +1,74 @@
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const ROOT = path.resolve('public');
|
||||
// Files NOT to touch
|
||||
const SKIP = new Set(['favicon.ico', 'apple-touch-icon.png', 'icon.png', 'apple-icon.png']);
|
||||
const SKIP_EXACT_PATHS = new Set([
|
||||
'public/favicon.ico',
|
||||
'public/apple-touch-icon.png',
|
||||
]);
|
||||
|
||||
const exts = new Set(['.jpg', '.jpeg', '.png', '.gif']);
|
||||
const results = { converted: 0, savedBytes: 0, skipped: 0, failed: 0 };
|
||||
|
||||
async function walk(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
await walk(p);
|
||||
} else if (exts.has(path.extname(p).toLowerCase())) {
|
||||
await convert(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function convert(input) {
|
||||
const ext = path.extname(input).toLowerCase();
|
||||
const base = input.slice(0, -ext.length);
|
||||
const output = base + '.webp';
|
||||
const relRoot = path.relative(process.cwd(), input).split(path.sep).join('/');
|
||||
if (SKIP.has(path.basename(input)) || SKIP_EXACT_PATHS.has(relRoot)) {
|
||||
results.skipped++;
|
||||
console.log(`SKIP ${relRoot}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const stIn = await fs.stat(input);
|
||||
// Output options per type
|
||||
const isPng = ext === '.png';
|
||||
const isGif = ext === '.gif';
|
||||
const opts = isPng
|
||||
? { quality: 86, alphaQuality: 90, effort: 5, nearLossless: false }
|
||||
: isGif
|
||||
? { quality: 80, effort: 5, loop: 0 } // animated -> animated webp
|
||||
: { quality: 82, effort: 5 };
|
||||
let pipeline = sharp(input, { animated: isGif });
|
||||
await pipeline.webp(opts).toFile(output);
|
||||
const stOut = await fs.stat(output);
|
||||
if (stOut.size >= stIn.size) {
|
||||
// worse than original — keep original, remove webp
|
||||
await fs.unlink(output);
|
||||
results.skipped++;
|
||||
console.log(`KEEP ${relRoot} (webp larger: ${stOut.size} vs ${stIn.size})`);
|
||||
return;
|
||||
}
|
||||
await fs.unlink(input);
|
||||
results.converted++;
|
||||
results.savedBytes += stIn.size - stOut.size;
|
||||
const inKB = (stIn.size / 1024).toFixed(0);
|
||||
const outKB = (stOut.size / 1024).toFixed(0);
|
||||
console.log(`OK ${relRoot} ${inKB}KB -> ${outKB}KB`);
|
||||
} catch (err) {
|
||||
results.failed++;
|
||||
console.log(`FAIL ${relRoot}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await walk(ROOT);
|
||||
const savedMB = (results.savedBytes / 1024 / 1024).toFixed(2);
|
||||
console.log('\n---');
|
||||
console.log(`converted: ${results.converted} skipped: ${results.skipped} failed: ${results.failed}`);
|
||||
console.log(`saved: ${savedMB} MB`);
|
||||
Reference in New Issue
Block a user