perf: create optimized thumbnails for project cards - 96% size reduction (615KB -> 24KB) for card preview images

This commit is contained in:
kinou-p 2025-10-02 18:54:37 +02:00
parent 1e73530afa
commit 15993914f1
10 changed files with 89 additions and 5 deletions

View File

@ -11,6 +11,7 @@
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"optimize-images": "node scripts/optimize-images.js", "optimize-images": "node scripts/optimize-images.js",
"create-thumbnails": "node scripts/create-thumbnails.js",
"analyze": "node scripts/analyze-bundle.js" "analyze": "node scripts/analyze-bundle.js"
}, },
"dependencies": { "dependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,83 @@
import sharp from 'sharp';
import { readdir, stat } from 'fs/promises';
import { join, extname, dirname, basename } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const publicDir = join(__dirname, '..', 'public', 'images');
// Liste des images utilisées dans les cartes projet (miniatures)
const thumbnailImages = [
'projects/homemade_nas.webp',
'projects/pong.webp',
'projects/cloud_1.webp',
'projects/minishell.webp',
'projects/cub3d.webp',
'sites/avopieces/mookup/3-devices-white (1).webp',
'sites/etsidemain/mookup/3-devices-white.webp'
];
async function createThumbnail(filePath, size = 128) {
const ext = extname(filePath);
const dir = dirname(filePath);
const name = basename(filePath, ext);
const thumbnailPath = join(dir, `${name}_thumb${ext}`);
try {
const stats = await stat(filePath);
const originalSize = stats.size;
// Créer une miniature carrée de 128x128
await sharp(filePath)
.resize(size, size, {
fit: 'cover',
position: 'center'
})
.webp({
quality: 85,
effort: 6
})
.toFile(thumbnailPath);
const newStats = await stat(thumbnailPath);
const newSize = newStats.size;
const reduction = ((originalSize - newSize) / originalSize * 100).toFixed(2);
console.log(`${basename(filePath)}`);
console.log(` Original: ${(originalSize / 1024).toFixed(2)} KB`);
console.log(` Miniature: ${(newSize / 1024).toFixed(2)} KB`);
console.log(` Réduction: ${reduction}%\n`);
return { originalSize, newSize };
} catch (error) {
console.error(`❌ Erreur avec ${filePath}:`, error.message);
return { originalSize: 0, newSize: 0 };
}
}
async function main() {
console.log('🖼️ Création des miniatures pour les cartes projet...\n');
let totalOriginal = 0;
let totalNew = 0;
for (const imagePath of thumbnailImages) {
const fullPath = join(publicDir, imagePath);
const { originalSize, newSize } = await createThumbnail(fullPath);
totalOriginal += originalSize;
totalNew += newSize;
}
const totalReduction = ((totalOriginal - totalNew) / totalOriginal * 100).toFixed(2);
console.log('=' .repeat(50));
console.log(`📊 RÉSUMÉ DES MINIATURES:`);
console.log(` Taille originale: ${(totalOriginal / 1024).toFixed(2)} KB`);
console.log(` Taille miniatures: ${(totalNew / 1024).toFixed(2)} KB`);
console.log(` Économie totale: ${totalReduction}%`);
console.log(` ${(totalOriginal - totalNew) / 1024} KB économisés! 🎉`);
}
main().catch(console.error);

View File

@ -28,22 +28,22 @@ export const ProjectCard = ({ title, description, icon, image, technologies, del
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.5, delay }} transition={{ duration: 0.5, delay }}
whileHover={{ y: -5 }} whileHover={{ y: -5, transition: { duration: 0.2 } }}
onClick={handleClick} onClick={handleClick}
className={projectId ? "cursor-pointer" : ""} className={projectId ? "cursor-pointer" : ""}
> >
<Card className="h-full hover:shadow-lg transition-all duration-300 border-border/50 bg-card/50 backdrop-blur relative overflow-hidden group"> <Card className="h-full hover:shadow-lg transition-all duration-200 border-border/50 bg-card/50 backdrop-blur relative overflow-hidden group">
{/* Indicateur cliquable en bas à droite */} {/* Indicateur cliquable en bas à droite */}
{projectId && ( {projectId && (
<div className="absolute bottom-4 right-4 w-8 h-8 rounded-full bg-primary/10 group-hover:bg-primary/20 flex items-center justify-center transition-all duration-300 group-hover:scale-110"> <div className="absolute bottom-4 right-4 w-8 h-8 rounded-full bg-primary/10 group-hover:bg-primary/20 flex items-center justify-center transition-all duration-200 group-hover:scale-110">
<ArrowRight className="w-4 h-4 text-primary group-hover:translate-x-0.5 transition-transform duration-300" /> <ArrowRight className="w-4 h-4 text-primary group-hover:translate-x-0.5 transition-transform duration-200" />
</div> </div>
)} )}
{image && ( {image && (
<div className="absolute top-4 right-4 w-16 h-16 rounded-lg overflow-hidden border-2 border-background/20 shadow-lg"> <div className="absolute top-4 right-4 w-16 h-16 rounded-lg overflow-hidden border-2 border-background/20 shadow-lg">
<img <img
src={image} src={image.replace('.webp', '_thumb.webp')}
alt={`${title} preview`} alt={`${title} preview`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />