From 63013816c82fd6ab053fd51a5db442efcf4c34e6 Mon Sep 17 00:00:00 2001 From: HeagBoKaT Date: Fri, 26 Jun 2026 09:55:49 +0300 Subject: [PATCH] =?UTF-8?q?perf:=20optimize=20image=20loading=20=E2=80=94?= =?UTF-8?q?=20resize,=20thumbnails,=20caching,=20lazy=20load?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- client/src/components/CatCard.tsx | 19 ++++++++++---- client/src/pages/FeedPage.tsx | 4 +-- server/src/index.ts | 5 +++- server/src/routes/cats.ts | 11 +++++++- server/src/scripts/gen-thumbs.ts | 43 +++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 server/src/scripts/gen-thumbs.ts diff --git a/client/src/components/CatCard.tsx b/client/src/components/CatCard.tsx index 418b7ba..35844d2 100644 --- a/client/src/components/CatCard.tsx +++ b/client/src/components/CatCard.tsx @@ -11,12 +11,18 @@ interface CatCardProps { cat: Cat; onOpen: (id: number) => void; likedIds?: Set; + priority?: boolean; } -export default function CatCard({ cat, onOpen, likedIds }: CatCardProps) { +export default function CatCard({ cat, onOpen, likedIds, priority }: CatCardProps) { const { user } = useAuth(); const [liked, setLiked] = useState(() => likedIds?.has(cat.id) ?? false); const [likeCount, setLikeCount] = useState(cat.likes_count); + const [imgLoaded, setImgLoaded] = useState(false); + + const thumbSrc = cat.image_url.endsWith('.gif') + ? cat.image_url + : cat.image_url.replace(/\.jpg$/, '_thumb.jpg'); const { toast } = useToast(); const likeMutation = useLikeCat(cat.id); const unlikeMutation = useUnlikeCat(cat.id); @@ -75,13 +81,16 @@ export default function CatCard({ cat, onOpen, likedIds }: CatCardProps) { {/* Изображение */} -
+
{cat.caption setImgLoaded(true)} + onError={e => { e.currentTarget.src = cat.image_url; }} />
diff --git a/client/src/pages/FeedPage.tsx b/client/src/pages/FeedPage.tsx index 1c97c21..925e5eb 100644 --- a/client/src/pages/FeedPage.tsx +++ b/client/src/pages/FeedPage.tsx @@ -103,8 +103,8 @@ export default function FeedPage() { {allCats.length > 0 && ( <>
- {allCats.map((cat) => ( - + {allCats.map((cat, index) => ( + ))}
diff --git a/server/src/index.ts b/server/src/index.ts index e29d39c..854c032 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -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); diff --git a/server/src/routes/cats.ts b/server/src/routes/cats.ts index b6cd6c9..77fd292 100644 --- a/server/src/routes/cats.ts +++ b/server/src/routes/cats.ts @@ -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: 'Не удалось обработать изображение. Попробуйте другой формат.' }); diff --git a/server/src/scripts/gen-thumbs.ts b/server/src/scripts/gen-thumbs.ts new file mode 100644 index 0000000..85b6ddc --- /dev/null +++ b/server/src/scripts/gen-thumbs.ts @@ -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);