const sharp = require('sharp'); const fs = require('fs'); const path = require('path'); const PUBLIC_DIR = path.join(__dirname, '../public'); const WEBP_QUALITY = 80; const AVIF_QUALITY = 80; const PNG_QUALITY = 80; async function optimizeImages() { console.log('Starting image optimization...\n'); // Get all PNG files in public directory const files = fs.readdirSync(PUBLIC_DIR) .filter(file => file.endsWith('.png')) .map(file => path.join(PUBLIC_DIR, file)); if (files.length === 0) { console.log('No PNG files found in public directory'); return; } console.log(`Found ${files.length} PNG files to optimize\n`); let totalOriginalSize = 0; let totalWebpSize = 0; let totalAvifSize = 0; let totalCompressedPngSize = 0; for (const filePath of files) { const filename = path.basename(filePath); const filenameWithoutExt = path.basename(filePath, '.png'); const originalSize = fs.statSync(filePath).size; totalOriginalSize += originalSize; console.log(`Processing: ${filename} (${(originalSize / 1024 / 1024).toFixed(2)} MB)`); try { // Generate WebP version const webpPath = path.join(PUBLIC_DIR, `${filenameWithoutExt}.webp`); await sharp(filePath) .resize(1920, null, { withoutEnlargement: true, fit: 'inside' }) .webp({ quality: WEBP_QUALITY }) .toFile(webpPath); const webpSize = fs.statSync(webpPath).size; totalWebpSize += webpSize; console.log(` ✓ WebP: ${(webpSize / 1024).toFixed(2)} KB (${((1 - webpSize / originalSize) * 100).toFixed(1)}% reduction)`); // Generate AVIF version const avifPath = path.join(PUBLIC_DIR, `${filenameWithoutExt}.avif`); await sharp(filePath) .resize(1920, null, { withoutEnlargement: true, fit: 'inside' }) .avif({ quality: AVIF_QUALITY }) .toFile(avifPath); const avifSize = fs.statSync(avifPath).size; totalAvifSize += avifSize; console.log(` ✓ AVIF: ${(avifSize / 1024).toFixed(2)} KB (${((1 - avifSize / originalSize) * 100).toFixed(1)}% reduction)`); // Compress original PNG const compressedPngPath = path.join(PUBLIC_DIR, `${filenameWithoutExt}_compressed.png`); await sharp(filePath) .resize(1920, null, { withoutEnlargement: true, fit: 'inside' }) .png({ quality: PNG_QUALITY, compressionLevel: 9, palette: true }) .toFile(compressedPngPath); const compressedPngSize = fs.statSync(compressedPngPath).size; totalCompressedPngSize += compressedPngSize; console.log(` ✓ Compressed PNG: ${(compressedPngSize / 1024).toFixed(2)} KB (${((1 - compressedPngSize / originalSize) * 100).toFixed(1)}% reduction)`); // Replace original with compressed version fs.unlinkSync(filePath); fs.renameSync(compressedPngPath, filePath); console.log(` ✓ Original PNG replaced with compressed version\n`); } catch (error) { console.error(` ✗ Error processing ${filename}:`, error.message); } } // Summary console.log('\n' + '='.repeat(60)); console.log('OPTIMIZATION SUMMARY'); console.log('='.repeat(60)); console.log(`Total original PNG size: ${(totalOriginalSize / 1024 / 1024).toFixed(2)} MB`); console.log(`Total WebP size: ${(totalWebpSize / 1024 / 1024).toFixed(2)} MB (${((1 - totalWebpSize / totalOriginalSize) * 100).toFixed(1)}% reduction)`); console.log(`Total AVIF size: ${(totalAvifSize / 1024 / 1024).toFixed(2)} MB (${((1 - totalAvifSize / totalOriginalSize) * 100).toFixed(1)}% reduction)`); console.log(`Total compressed PNG size: ${(totalCompressedPngSize / 1024 / 1024).toFixed(2)} MB (${((1 - totalCompressedPngSize / totalOriginalSize) * 100).toFixed(1)}% reduction)`); console.log('\nAverage file sizes:'); console.log(` WebP: ${(totalWebpSize / files.length / 1024).toFixed(2)} KB`); console.log(` AVIF: ${(totalAvifSize / files.length / 1024).toFixed(2)} KB`); console.log(` Compressed PNG: ${(totalCompressedPngSize / files.length / 1024).toFixed(2)} KB`); console.log('='.repeat(60)); // Check if targets are met const avgWebpSize = totalWebpSize / files.length / 1024; const avgAvifSize = totalAvifSize / files.length / 1024; const totalSizeAfter = (totalWebpSize + totalAvifSize + totalCompressedPngSize) / 1024 / 1024; console.log('\nTarget Achievement:'); console.log(` WebP avg < 200KB: ${avgWebpSize < 200 ? '✓ PASS' : '✗ FAIL'} (${avgWebpSize.toFixed(2)} KB)`); console.log(` AVIF avg < 150KB: ${avgAvifSize < 150 ? '✓ PASS' : '✗ FAIL'} (${avgAvifSize.toFixed(2)} KB)`); console.log(` Total size < 2MB: ${totalSizeAfter < 2 ? '✓ PASS' : '✗ FAIL'} (${totalSizeAfter.toFixed(2)} MB)`); } optimizeImages() .then(() => console.log('\nOptimization complete!')) .catch(error => console.error('Optimization failed:', error));