|
|
#!/usr/bin/env node |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require('fs'); |
|
|
const path = require('path'); |
|
|
|
|
|
function getFileSize(filePath) { |
|
|
try { |
|
|
const stats = fs.statSync(filePath); |
|
|
return stats.size; |
|
|
} catch { |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
|
|
|
function formatSize(bytes) { |
|
|
return (bytes / 1024).toFixed(2) + ' KB'; |
|
|
} |
|
|
|
|
|
function analyzeImages(dir, extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']) { |
|
|
let images = []; |
|
|
|
|
|
try { |
|
|
const items = fs.readdirSync(dir); |
|
|
|
|
|
for (const item of items) { |
|
|
const fullPath = path.join(dir, item); |
|
|
const stat = fs.statSync(fullPath); |
|
|
|
|
|
if (stat.isDirectory()) { |
|
|
images = images.concat(analyzeImages(fullPath, extensions)); |
|
|
} else if (stat.isFile()) { |
|
|
const ext = path.extname(item).toLowerCase(); |
|
|
if (extensions.includes(ext)) { |
|
|
images.push({ |
|
|
path: fullPath.replace(process.cwd(), ''), |
|
|
name: item, |
|
|
size: stat.size, |
|
|
ext: ext |
|
|
}); |
|
|
} |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error(`Error analyzing directory ${dir}:`, error.message); |
|
|
} |
|
|
|
|
|
return images; |
|
|
} |
|
|
|
|
|
console.log('🖼️ Image Optimization Analysis\n'); |
|
|
console.log('='.repeat(60)); |
|
|
|
|
|
const publicDir = path.join(__dirname, '..', 'public'); |
|
|
const iconsDir = path.join(publicDir, 'icons'); |
|
|
|
|
|
|
|
|
const images = analyzeImages(publicDir); |
|
|
|
|
|
if (images.length === 0) { |
|
|
console.log('\nNo images found to optimize.'); |
|
|
process.exit(0); |
|
|
} |
|
|
|
|
|
console.log(`\nFound ${images.length} images\n`); |
|
|
|
|
|
|
|
|
const byType = {}; |
|
|
images.forEach(img => { |
|
|
if (!byType[img.ext]) { |
|
|
byType[img.ext] = []; |
|
|
} |
|
|
byType[img.ext].push(img); |
|
|
}); |
|
|
|
|
|
|
|
|
let totalSize = 0; |
|
|
let potentialSavings = 0; |
|
|
|
|
|
for (const [ext, imgs] of Object.entries(byType)) { |
|
|
console.log(`\n${ext.toUpperCase()} Images:`); |
|
|
console.log('-'.repeat(60)); |
|
|
|
|
|
const typeSize = imgs.reduce((sum, img) => sum + img.size, 0); |
|
|
totalSize += typeSize; |
|
|
|
|
|
|
|
|
let savings = 0; |
|
|
let recommendation = ''; |
|
|
|
|
|
switch (ext) { |
|
|
case '.png': |
|
|
savings = typeSize * 0.4; |
|
|
recommendation = 'Use pngquant or TinyPNG. Consider WebP format.'; |
|
|
break; |
|
|
case '.jpg': |
|
|
case '.jpeg': |
|
|
savings = typeSize * 0.3; |
|
|
recommendation = 'Use mozjpeg or jpegoptim. Consider WebP format.'; |
|
|
break; |
|
|
case '.svg': |
|
|
savings = typeSize * 0.2; |
|
|
recommendation = 'Use SVGO to optimize.'; |
|
|
break; |
|
|
case '.gif': |
|
|
savings = typeSize * 0.5; |
|
|
recommendation = 'Convert to WebP or use gifsicle.'; |
|
|
break; |
|
|
default: |
|
|
savings = 0; |
|
|
recommendation = 'Already optimized format.'; |
|
|
} |
|
|
|
|
|
potentialSavings += savings; |
|
|
|
|
|
|
|
|
imgs.sort((a, b) => b.size - a.size); |
|
|
imgs.slice(0, 5).forEach(img => { |
|
|
console.log(` ${img.name.padEnd(30)} ${formatSize(img.size).padStart(12)}`); |
|
|
}); |
|
|
|
|
|
if (imgs.length > 5) { |
|
|
console.log(` ... and ${imgs.length - 5} more`); |
|
|
} |
|
|
|
|
|
console.log(` Total: ${formatSize(typeSize)}`); |
|
|
console.log(` 💡 ${recommendation}`); |
|
|
console.log(` Potential savings: ${formatSize(savings)}`); |
|
|
} |
|
|
|
|
|
console.log('\n\n📊 Summary'); |
|
|
console.log('='.repeat(60)); |
|
|
console.log(` Total images: ${images.length}`); |
|
|
console.log(` Total size: ${formatSize(totalSize)}`); |
|
|
console.log(` Potential savings: ${formatSize(potentialSavings)} (${((potentialSavings/totalSize)*100).toFixed(1)}%)`); |
|
|
|
|
|
console.log('\n\n💡 Optimization Recommendations'); |
|
|
console.log('='.repeat(60)); |
|
|
|
|
|
console.log('\n1. Install optimization tools:'); |
|
|
console.log(' npm install -g svgo'); |
|
|
console.log(' # For PNG: brew install pngquant (macOS) or apt-get install pngquant (Linux)'); |
|
|
console.log(' # For JPEG: brew install mozjpeg (macOS) or apt-get install mozjpeg (Linux)'); |
|
|
|
|
|
console.log('\n2. Optimize SVG icons:'); |
|
|
console.log(' svgo -f public/icons -o public/icons-optimized'); |
|
|
|
|
|
console.log('\n3. Convert to WebP (best compression):'); |
|
|
console.log(' # For each image:'); |
|
|
console.log(' # cwebp input.png -q 80 -o output.webp'); |
|
|
|
|
|
console.log('\n4. Use modern image formats:'); |
|
|
console.log(' • WebP: Best compression, wide support'); |
|
|
console.log(' • AVIF: Better than WebP, limited support'); |
|
|
console.log(' • Provide fallbacks for older browsers'); |
|
|
|
|
|
console.log('\n5. Implement responsive images:'); |
|
|
console.log(' <picture>'); |
|
|
console.log(' <source srcset="image.webp" type="image/webp">'); |
|
|
console.log(' <source srcset="image.jpg" type="image/jpeg">'); |
|
|
console.log(' <img src="image.jpg" alt="...">'); |
|
|
console.log(' </picture>'); |
|
|
|
|
|
console.log('\n6. Use lazy loading:'); |
|
|
console.log(' <img src="..." loading="lazy" alt="...">'); |
|
|
|
|
|
console.log('\n\n🎯 Priority Actions:'); |
|
|
console.log('='.repeat(60)); |
|
|
|
|
|
|
|
|
const largeImages = images.filter(img => img.size > 100 * 1024).sort((a, b) => b.size - a.size); |
|
|
|
|
|
if (largeImages.length > 0) { |
|
|
console.log('\n⚠️ Large images that need optimization (>100KB):'); |
|
|
largeImages.slice(0, 10).forEach((img, i) => { |
|
|
console.log(` ${i + 1}. ${img.name} (${formatSize(img.size)})`); |
|
|
}); |
|
|
} else { |
|
|
console.log('\n✅ No large images found! All images are optimized.'); |
|
|
} |
|
|
|
|
|
|
|
|
const svgImages = images.filter(img => img.ext === '.svg'); |
|
|
if (svgImages.length > 0) { |
|
|
console.log(`\n📝 ${svgImages.length} SVG files can be optimized with SVGO`); |
|
|
} |
|
|
|
|
|
console.log('\n' + '='.repeat(60)); |
|
|
console.log('✅ Analysis complete!\n'); |
|
|
|
|
|
|