perf: optimize image loading — resize, thumbnails, caching, lazy load

- Server: resize uploads to max 1200px + generate 600px _thumb.jpg
- Server: cache /uploads with max-age=1y immutable headers
- Client: CatCard uses _thumb.jpg in feed (fallback to full on error)
- Client: first 3 feed cards load eagerly (LCP), rest lazy + decoding=async
- Client: loading placeholder bg while image fetches
- Migration: server/src/scripts/gen-thumbs.ts for existing images

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 09:55:49 +03:00
parent 852e0f5f5b
commit 63013816c8
5 changed files with 73 additions and 9 deletions

View File

@@ -0,0 +1,43 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import sharp from 'sharp';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const uploadsDir = path.join(__dirname, '..', '..', 'uploads');
async function main() {
const files = fs.readdirSync(uploadsDir);
const candidates = files.filter(
f => f.endsWith('.jpg') && !f.endsWith('_thumb.jpg')
);
console.log(`Found ${candidates.length} images to process`);
let created = 0;
let skipped = 0;
for (const filename of candidates) {
const thumbFilename = filename.replace(/\.jpg$/, '_thumb.jpg');
const thumbPath = path.join(uploadsDir, thumbFilename);
if (fs.existsSync(thumbPath)) {
skipped++;
continue;
}
try {
await sharp(path.join(uploadsDir, filename))
.resize({ width: 600, withoutEnlargement: true })
.jpeg({ quality: 80 })
.toFile(thumbPath);
created++;
console.log(`${thumbFilename}`);
} catch (err) {
console.error(`${filename}: ${err}`);
}
}
console.log(`\nDone: ${created} created, ${skipped} already existed`);
}
main().catch(console.error);