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:
@@ -60,7 +60,10 @@ async function main() {
|
||||
return uploadLimiter(req, _res, next);
|
||||
});
|
||||
|
||||
app.use('/uploads', express.static(path.join(__dirname, '..', 'uploads')));
|
||||
app.use('/uploads', express.static(path.join(__dirname, '..', 'uploads'), {
|
||||
maxAge: '365d',
|
||||
immutable: true,
|
||||
}));
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/cats', catRoutes);
|
||||
|
||||
@@ -127,8 +127,17 @@ router.post('/', authMiddleware, upload.single('image'), async (req: AuthRequest
|
||||
finalFilename = req.file.filename.replace(/\.[^.]+$/, '.jpg');
|
||||
finalPath = path.join(uploadsDir, finalFilename);
|
||||
try {
|
||||
await sharp(origPath).jpeg({ quality: 85 }).toFile(finalPath);
|
||||
await sharp(origPath)
|
||||
.resize({ width: 1200, withoutEnlargement: true })
|
||||
.jpeg({ quality: 85 })
|
||||
.toFile(finalPath);
|
||||
if (origPath !== finalPath) fs.unlinkSync(origPath);
|
||||
|
||||
const thumbFilename = finalFilename.replace(/\.jpg$/, '_thumb.jpg');
|
||||
await sharp(finalPath)
|
||||
.resize({ width: 600, withoutEnlargement: true })
|
||||
.jpeg({ quality: 80 })
|
||||
.toFile(path.join(uploadsDir, thumbFilename));
|
||||
} catch {
|
||||
try { fs.unlinkSync(origPath); } catch {}
|
||||
res.status(400).json({ error: 'Не удалось обработать изображение. Попробуйте другой формат.' });
|
||||
|
||||
43
server/src/scripts/gen-thumbs.ts
Normal file
43
server/src/scripts/gen-thumbs.ts
Normal 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);
|
||||
Reference in New Issue
Block a user