Image Optimization and Format Detection: A Developer's Guide
A practical Fusebox guide to image optimization and format detection.
Image Optimization and Format Detection: A Developer's Guide
Images make up 50% of the average webpage's weight. A single unoptimized hero image can add 5MB to your page, causing users to abandon before they even see your content. This guide provides JavaScript tools to detect image formats, analyze optimization opportunities, and implement modern image delivery strategies.
Why Image Optimization Matters
In 2021, an e-commerce site reduced their bounce rate by 40% simply by optimizing product images. They discovered their photographers were uploading 10MB RAW files directly to the site. Pinterest increased their signup rate by 15% after implementing WebP with proper fallbacks. Image optimization isn't just about file size—it's about user experience and conversion rates.
Image Format Detector and Analyzer
Comprehensive image analysis tool:
// Image Format Detector and Analyzer
class ImageAnalyzer {
constructor() {
this.images = [];
this.formats = {
jpeg: { count: 0, totalSize: 0, signatures: [0xFF, 0xD8, 0xFF] },
png: { count: 0, totalSize: 0, signatures: [0x89, 0x50, 0x4E, 0x47] },
gif: { count: 0, totalSize: 0, signatures: [0x47, 0x49, 0x46] },
webp: { count: 0, totalSize: 0, signatures: [0x52, 0x49, 0x46, 0x46] },
avif: { count: 0, totalSize: 0, signatures: [] }, // Complex detection
svg: { count: 0, totalSize: 0, signatures: [] } // XML-based
};
}
// Analyze all images on page
async analyzePageImages() {
const images = document.querySelectorAll('img, picture source');
const imageData = [];
for (const img of images) {
const src = img.src || img.srcset?.split(' ')[0];
if (!src) continue;
try {
const analysis = await this.analyzeImage(src);
imageData.push(analysis);
} catch (error) {
console.error(`Failed to analyze ${src}:`, error);
}
}
this.images = imageData;
return this.generateReport();
}
// Analyze individual image
async analyzeImage(url) {
const startTime = performance.now();
// Fetch image
const response = await fetch(url);
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// Detect format
const format = this.detectFormat(uint8Array, blob.type);
// Get dimensions
const dimensions = await this.getImageDimensions(url);
// Analyze optimization
const optimization = this.analyzeOptimization(uint8Array, format, dimensions);
const analysis = {
url,
format,
mimeType: blob.type,
fileSize: blob.size,
dimensions,
loadTime: performance.now() - startTime,
optimization,
// Check if image is lazy loaded
isLazyLoaded: this.checkLazyLoading(url),
// Check responsive images
responsive: this.checkResponsiveImages(url),
// Estimate quality
estimatedQuality: this.estimateQuality(uint8Array, format)
};
// Update format statistics
if (this.formats[format]) {
this.formats[format].count++;
this.formats[format].totalSize += blob.size;
}
return analysis;
}
// Detect image format from bytes
detectFormat(bytes, mimeType) {
// Check JPEG
if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {
return 'jpeg';
}
// Check PNG
if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {
return 'png';
}
// Check GIF
if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {
return 'gif';
}
// Check WebP
if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46) {
if (bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {
return 'webp';
}
}
// Check AVIF (more complex)
if (mimeType === 'image/avif') {
return 'avif';
}
// Check SVG
if (mimeType === 'image/svg+xml') {
return 'svg';
}
return 'unknown';
}
// Get image dimensions
async getImageDimensions(url) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
resolve({
width: img.naturalWidth,
height: img.naturalHeight,
aspectRatio: (img.naturalWidth / img.naturalHeight).toFixed(2),
displayWidth: img.width || img.naturalWidth,
displayHeight: img.height || img.naturalHeight
});
};
img.onerror = () => resolve({ width: 0, height: 0 });
img.src = url;
});
}
// Analyze optimization opportunities
analyzeOptimization(bytes, format, dimensions) {
const optimization = {
canOptimize: false,
suggestions: [],
potentialSavings: 0,
recommendedFormat: null
};
// Check if image is too large for display size
if (dimensions.width > dimensions.displayWidth * 2) {
optimization.canOptimize = true;
optimization.suggestions.push({
type: 'resize',
message: `Image is ${dimensions.width}px wide but displayed at ${dimensions.displayWidth}px`,
savings: Math.round(bytes.length * 0.75) // Rough estimate
});
}
// Format-specific optimizations
switch (format) {
case 'png':
// PNG for photos is usually suboptimal
if (bytes.length > 100000) { // 100KB
optimization.canOptimize = true;
optimization.suggestions.push({
type: 'format',
message: 'Consider JPEG for photographic content',
savings: Math.round(bytes.length * 0.7)
});
optimization.recommendedFormat = 'jpeg';
}
break;
case 'jpeg':
// Check for progressive JPEG
if (!this.isProgressiveJPEG(bytes)) {
optimization.suggestions.push({
type: 'progressive',
message: 'Convert to progressive JPEG for better perceived performance'
});
}
// Suggest WebP
optimization.suggestions.push({
type: 'format',
message: 'WebP could save 25-35% file size',
savings: Math.round(bytes.length * 0.3)
});
optimization.recommendedFormat = 'webp';
break;
case 'gif':
optimization.canOptimize = true;
optimization.suggestions.push({
type: 'format',
message: 'Replace GIF with video (MP4) for 90% size reduction',
savings: Math.round(bytes.length * 0.9)
});
break;
}
// Calculate total potential savings
optimization.potentialSavings = optimization.suggestions
.reduce((total, s) => total + (s.savings || 0), 0);
return optimization;
}
// Check if JPEG is progressive
isProgressiveJPEG(bytes) {
// Look for SOF2 marker (0xFFC2) which indicates progressive
for (let i = 0; i < bytes.length - 1; i++) {
if (bytes[i] === 0xFF && bytes[i + 1] === 0xC2) {
return true;
}
}
return false;
}
// Estimate image quality
estimateQuality(bytes, format) {
if (format === 'jpeg') {
// Simple quality estimation based on quantization tables
// This is a rough approximation
const fileSize = bytes.length;
for (let i = 0; i < bytes.length - 4; i++) {
if (bytes[i] === 0xFF && bytes[i + 1] === 0xDB) {
// Found quantization table
const precision = bytes[i + 4] >> 4;
const tableData = bytes[i + 5];
// Very rough quality estimate
if (tableData < 10) return 95;
if (tableData < 25) return 85;
if (tableData < 50) return 75;
if (tableData < 100) return 60;
return 50;
}
}
}
return null;
}
// Check lazy loading
checkLazyLoading(url) {
const img = document.querySelector(`img[src="${url}"]`);
if (!img) return false;
return img.loading === 'lazy' ||
img.hasAttribute('data-lazy') ||
img.classList.contains('lazyload');
}
// Check responsive images
checkResponsiveImages(url) {
const img = document.querySelector(`img[src="${url}"]`);
if (!img) return { isResponsive: false };
const picture = img.closest('picture');
const srcset = img.srcset;
return {
isResponsive: !!(picture || srcset),
hasPicture: !!picture,
hasSrcset: !!srcset,
sources: picture ? picture.querySelectorAll('source').length : 0
};
}
// Generate comprehensive report
generateReport() {
const report = {
summary: {
totalImages: this.images.length,
totalSize: this.images.reduce((sum, img) => sum + img.fileSize, 0),
averageSize: Math.round(this.images.reduce((sum, img) => sum + img.fileSize, 0) / this.images.length),
formats: this.formats,
lazyLoaded: this.images.filter(img => img.isLazyLoaded).length,
responsive: this.images.filter(img => img.responsive.isResponsive).length
},
optimizationOpportunities: {
totalPotentialSavings: this.images.reduce((sum, img) =>
sum + img.optimization.potentialSavings, 0),
count: this.images.filter(img => img.optimization.canOptimize).length
},
images: this.images.sort((a, b) => b.fileSize - a.fileSize),
recommendations: this.generateRecommendations()
};
return report;
}
// Generate recommendations
generateRecommendations() {
const recommendations = [];
// Check for missing modern formats
if (this.formats.webp.count === 0 && this.formats.jpeg.count > 0) {
recommendations.push({
priority: 'high',
type: 'format',
message: 'Implement WebP format with JPEG fallback',
impact: 'Save 25-35% on image file sizes',
implementation: `<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description">
</picture>`
});
}
// Check for missing lazy loading
const lazyLoadPercentage = (this.images.filter(img => img.isLazyLoaded).length / this.images.length) * 100;
if (lazyLoadPercentage < 50) {
recommendations.push({
priority: 'high',
type: 'performance',
message: 'Implement lazy loading for below-the-fold images',
impact: 'Reduce initial page load by 50%+',
implementation: '<img src="image.jpg" loading="lazy" alt="Description">'
});
}
// Check for oversized images
const oversizedImages = this.images.filter(img => img.fileSize > 500000); // 500KB
if (oversizedImages.length > 0) {
recommendations.push({
priority: 'critical',
type: 'optimization',
message: `${oversizedImages.length} images are over 500KB`,
impact: 'Significant performance impact on mobile',
images: oversizedImages.map(img => ({
url: img.url,
size: img.fileSize,
format: img.format
}))
});
}
return recommendations;
}
}
// Run analysis
const analyzer = new ImageAnalyzer();
analyzer.analyzePageImages().then(report => {
console.log('Image Optimization Report:', report);
});
Modern Image Format Implementation
Implement next-gen image formats with fallbacks:
// Modern Image Format Manager
class ModernImageManager {
constructor() {
this.supportedFormats = this.detectFormatSupport();
this.observer = null;
}
// Detect browser format support
detectFormatSupport() {
const formats = {
webp: false,
avif: false,
jpeg2000: false,
jpegxr: false
};
// Test WebP support
const webpData = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=';
const webpImg = new Image();
webpImg.onload = () => { formats.webp = true; };
webpImg.src = webpData;
// Test AVIF support
const avifData = 'data:image/avif;base64,AAAAHGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZgAAAPBtZXRhAAAAAAAAABloZGxyAAAAAAAAAABwaWN0AAAAAAAAAAAAAAAAbGliYXZpZgAAAAAOcGl0bQAAAAAAAQAAAB5pbG9jAAAAAEQAAAEAAQAAAAEAAAEUAAAAJgAAAChpaW5mAAAAAAABAAAAGmluZmUCAAAAAAEAAGF2MDEAAAAAamlwcnAAAABLaXBjbwAAABNjb2xybmNseAABAA0AAIAAAAAMYXYxQ4EBDAAAAABtZGF0EgAKBzgABps9JaeBvww=';
const avifImg = new Image();
avifImg.onload = () => { formats.avif = true; };
avifImg.src = avifData;
return formats;
}
// Convert image URLs to modern format
getModernImageUrl(originalUrl, preferredFormat = null) {
const extension = originalUrl.split('.').pop().toLowerCase();
const basePath = originalUrl.substring(0, originalUrl.lastIndexOf('.'));
// Determine best format
let modernFormat = preferredFormat;
if (!modernFormat) {
if (this.supportedFormats.avif) {
modernFormat = 'avif';
} else if (this.supportedFormats.webp) {
modernFormat = 'webp';
}
}
if (!modernFormat) return originalUrl;
// Return modern format URL
return `${basePath}.${modernFormat}`;
}
// Generate picture element with fallbacks
generatePictureElement(src, alt, sizes = null) {
const sources = [];
const basePath = src.substring(0, src.lastIndexOf('.'));
const extension = src.split('.').pop().toLowerCase();
// Add AVIF source
if (this.supportedFormats.avif) {
sources.push({
srcset: `${basePath}.avif`,
type: 'image/avif'
});
}
// Add WebP source
if (this.supportedFormats.webp) {
sources.push({
srcset: `${basePath}.webp`,
type: 'image/webp'
});
}
// Generate HTML
let html = '<picture>\n';
sources.forEach(source => {
html += ` <source srcset="${source.srcset}" type="${source.type}"`;
if (sizes) html += ` sizes="${sizes}"`;
html += '>\n';
});
html += ` <img src="${src}" alt="${alt}"`;
if (sizes) html += ` sizes="${sizes}"`;
html += ' loading="lazy">\n';
html += '</picture>';
return html;
}
// Implement responsive images
generateResponsiveImage(src, alt, breakpoints = {}) {
const defaultBreakpoints = {
small: { width: 640, maxWidth: '640px' },
medium: { width: 1024, maxWidth: '1024px' },
large: { width: 1920, maxWidth: '1920px' }
};
const bp = { ...defaultBreakpoints, ...breakpoints };
const sources = [];
// Generate sources for each format and breakpoint
['avif', 'webp', 'jpg'].forEach(format => {
if (format === 'jpg' || this.supportedFormats[format]) {
const srcset = Object.entries(bp)
.map(([size, config]) =>
`${src.replace(/\.[^.]+$/, '')}-${config.width}w.${format} ${config.width}w`
)
.join(', ');
sources.push({
srcset,
type: format === 'jpg' ? 'image/jpeg' : `image/${format}`
});
}
});
// Generate sizes attribute
const sizes = Object.entries(bp)
.map(([size, config]) => `(max-width: ${config.maxWidth}) ${config.width}px`)
.join(', ') + ', 100vw';
return this.generatePictureElementFromSources(sources, src, alt, sizes);
}
// Generate picture element from sources
generatePictureElementFromSources(sources, fallbackSrc, alt, sizes) {
let html = '<picture>\n';
sources.forEach(source => {
html += ` <source\n`;
html += ` srcset="${source.srcset}"\n`;
html += ` type="${source.type}"\n`;
if (sizes) html += ` sizes="${sizes}"\n`;
html += ' >\n';
});
html += ` <img\n`;
html += ` src="${fallbackSrc}"\n`;
html += ` alt="${alt}"\n`;
if (sizes) html += ` sizes="${sizes}"\n`;
html += ' loading="lazy"\n';
html += ' decoding="async"\n';
html += ' >\n';
html += '</picture>';
return html;
}
// Setup lazy loading with Intersection Observer
setupLazyLoading() {
if ('IntersectionObserver' in window) {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.01
});
// Observe all lazy images
document.querySelectorAll('img[data-src], picture[data-src]').forEach(img => {
this.observer.observe(img);
});
} else {
// Fallback for browsers without Intersection Observer
this.loadAllImages();
}
}
// Load individual image
loadImage(element) {
if (element.tagName === 'IMG') {
element.src = element.dataset.src;
element.removeAttribute('data-src');
} else if (element.tagName === 'PICTURE') {
const sources = element.querySelectorAll('source[data-srcset]');
sources.forEach(source => {
source.srcset = source.dataset.srcset;
source.removeAttribute('data-srcset');
});
const img = element.querySelector('img[data-src]');
if (img) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
}
element.classList.add('loaded');
}
// Load all images (fallback)
loadAllImages() {
document.querySelectorAll('img[data-src], picture[data-src]').forEach(element => {
this.loadImage(element);
});
}
}
// Initialize
const imageManager = new ModernImageManager();
imageManager.setupLazyLoading();
Image Loading Performance Monitor
Monitor and optimize image loading:
// Image Performance Monitor
class ImagePerformanceMonitor {
constructor() {
this.metrics = {
images: [],
lcp: null,
totalLoadTime: 0,
totalSize: 0
};
this.setupObservers();
}
// Setup performance observers
setupObservers() {
// Monitor resource timing for images
if ('PerformanceObserver' in window) {
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'img' ||
entry.initiatorType === 'css' || // Background images
entry.name.match(/\.(jpg|jpeg|png|gif|webp|avif|svg)/i)) {
this.trackImagePerformance(entry);
}
}
});
resourceObserver.observe({ entryTypes: ['resource'] });
// Monitor LCP
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry.element && lastEntry.element.tagName === 'IMG') {
this.metrics.lcp = {
element: lastEntry.element,
url: lastEntry.url,
loadTime: lastEntry.loadTime,
renderTime: lastEntry.renderTime,
size: lastEntry.size
};
}
});
try {
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
} catch (e) {
console.log('LCP observation not supported');
}
}
}
// Track individual image performance
trackImagePerformance(entry) {
const imageMetrics = {
url: entry.name,
startTime: entry.startTime,
duration: entry.duration,
transferSize: entry.transferSize || 0,
encodedBodySize: entry.encodedBodySize || 0,
decodedBodySize: entry.decodedBodySize || 0,
// Connection timing
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
ssl: entry.secureConnectionStart > 0 ?
entry.connectEnd - entry.secureConnectionStart : 0,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart,
// Cache status
fromCache: entry.transferSize === 0 && entry.decodedBodySize > 0,
// Protocol
protocol: entry.nextHopProtocol,
// Priority hints
priority: this.getImagePriority(entry.name)
};
this.metrics.images.push(imageMetrics);
this.metrics.totalLoadTime += imageMetrics.duration;
this.metrics.totalSize += imageMetrics.transferSize;
// Check for performance issues
this.checkPerformanceIssues(imageMetrics);
}
// Get image loading priority
getImagePriority(url) {
const img = document.querySelector(`img[src="${url}"]`);
if (!img) return 'unknown';
// Check fetchpriority attribute
if (img.fetchpriority) return img.fetchpriority;
// Check if in viewport
const rect = img.getBoundingClientRect();
const inViewport = rect.top < window.innerHeight && rect.bottom > 0;
return inViewport ? 'high' : 'low';
}
// Check for performance issues
checkPerformanceIssues(metrics) {
const issues = [];
// Slow TTFB
if (metrics.ttfb > 600) {
issues.push({
type: 'slow-ttfb',
message: `Slow server response: ${metrics.ttfb.toFixed(0)}ms`,
severity: 'high'
});
}
// Large transfer size
if (metrics.transferSize > 500000) { // 500KB
issues.push({
type: 'large-size',
message: `Large image: ${(metrics.transferSize / 1024 / 1024).toFixed(2)}MB`,
severity: 'high'
});
}
// Not cached
if (!metrics.fromCache && metrics.url.includes(window.location.hostname)) {
issues.push({
type: 'not-cached',
message: 'Image not served from cache',
severity: 'medium'
});
}
// Slow total load time
if (metrics.duration > 3000) {
issues.push({
type: 'slow-load',
message: `Slow load time: ${metrics.duration.toFixed(0)}ms`,
severity: 'high'
});
}
if (issues.length > 0) {
metrics.issues = issues;
}
}
// Generate performance report
generateReport() {
const report = {
summary: {
totalImages: this.metrics.images.length,
totalSize: this.metrics.totalSize,
totalLoadTime: this.metrics.totalLoadTime,
averageLoadTime: this.metrics.totalLoadTime / this.metrics.images.length,
cachedImages: this.metrics.images.filter(img => img.fromCache).length,
lcpImage: this.metrics.lcp
},
byProtocol: this.groupByProtocol(),
performanceIssues: this.getPerformanceIssues(),
recommendations: this.generateOptimizationRecommendations(),
timeline: this.generateLoadTimeline()
};
return report;
}
// Group images by protocol
groupByProtocol() {
const protocols = {};
this.metrics.images.forEach(img => {
const protocol = img.protocol || 'unknown';
if (!protocols[protocol]) {
protocols[protocol] = {
count: 0,
totalSize: 0,
averageLoadTime: 0
};
}
protocols[protocol].count++;
protocols[protocol].totalSize += img.transferSize;
protocols[protocol].averageLoadTime += img.duration;
});
// Calculate averages
Object.values(protocols).forEach(data => {
data.averageLoadTime = data.averageLoadTime / data.count;
});
return protocols;
}
// Get all performance issues
getPerformanceIssues() {
return this.metrics.images
.filter(img => img.issues && img.issues.length > 0)
.map(img => ({
url: img.url,
issues: img.issues,
impact: img.duration
}))
.sort((a, b) => b.impact - a.impact);
}
// Generate optimization recommendations
generateOptimizationRecommendations() {
const recommendations = [];
// Check for missing HTTP/2
const http1Images = this.metrics.images.filter(img =>
img.protocol && img.protocol.includes('http/1'));
if (http1Images.length > 0) {
recommendations.push({
type: 'protocol',
priority: 'high',
message: `${http1Images.length} images served over HTTP/1.1`,
impact: 'Enable HTTP/2 for parallel loading',
images: http1Images.map(img => img.url)
});
}
// Check for missing lazy loading on large images
const largeEagerImages = this.metrics.images
.filter(img => img.transferSize > 100000 && img.priority === 'high')
.filter(img => {
const element = document.querySelector(`img[src="${img.url}"]`);
return element && element.loading !== 'lazy';
});
if (largeEagerImages.length > 0) {
recommendations.push({
type: 'lazy-loading',
priority: 'high',
message: `${largeEagerImages.length} large images loaded eagerly`,
impact: 'Add loading="lazy" to reduce initial load',
savings: largeEagerImages.reduce((sum, img) => sum + img.transferSize, 0)
});
}
// Check for opportunities to use srcset
const nonResponsiveImages = this.metrics.images.filter(img => {
const element = document.querySelector(`img[src="${img.url}"]`);
return element && !element.srcset && !element.closest('picture');
});
if (nonResponsiveImages.length > 0) {
recommendations.push({
type: 'responsive',
priority: 'medium',
message: `${nonResponsiveImages.length} images without responsive alternatives`,
impact: 'Implement srcset for optimal image delivery'
});
}
return recommendations;
}
// Generate load timeline
generateLoadTimeline() {
return this.metrics.images
.sort((a, b) => a.startTime - b.startTime)
.map(img => ({
url: img.url.split('/').pop(),
start: img.startTime,
end: img.startTime + img.duration,
duration: img.duration,
size: img.transferSize
}));
}
}
// Start monitoring
const performanceMonitor = new ImagePerformanceMonitor();
// Get report after page load
window.addEventListener('load', () => {
setTimeout(() => {
console.log('Image Performance Report:', performanceMonitor.generateReport());
}, 2000);
});
Image Optimization Automation
Automate image optimization recommendations:
// Image Optimization Automator
class ImageOptimizationAutomator {
constructor() {
this.optimizations = [];
}
// Generate optimization script
async generateOptimizationScript() {
const images = await this.collectAllImages();
const script = {
bash: this.generateBashScript(images),
node: this.generateNodeScript(images),
html: this.generateHTMLUpdates(images)
};
return script;
}
// Collect all images
async collectAllImages() {
const images = [];
const elements = document.querySelectorAll('img, picture source');
for (const element of elements) {
const src = element.src || element.srcset?.split(' ')[0];
if (!src) continue;
try {
const response = await fetch(src);
const blob = await response.blob();
images.push({
url: src,
size: blob.size,
type: blob.type,
element: element.tagName.toLowerCase(),
hasAlt: element.alt ? true : false,
hasLazyLoading: element.loading === 'lazy',
hasSrcset: !!element.srcset
});
} catch (e) {
console.error(`Failed to analyze ${src}`);
}
}
return images;
}
// Generate bash optimization script
generateBashScript(images) {
let script = `#!/bin/bash
# Image Optimization Script
# Generated on ${new Date().toISOString()}
# Install required tools
echo "Installing image optimization tools..."
npm install -g imagemin-cli imagemin-webp imagemin-avif sharp-cli
# Create optimized directory
mkdir -p optimized/{webp,avif,compressed}
`;
images.forEach(img => {
const filename = img.url.split('/').pop();
const name = filename.substring(0, filename.lastIndexOf('.'));
const ext = filename.split('.').pop();
script += `
# Optimize ${filename}
echo "Processing ${filename}..."
# Generate WebP
imagemin ${filename} --plugin=webp --out-dir=optimized/webp
# Generate AVIF
imagemin ${filename} --plugin=avif --out-dir=optimized/avif
# Compress original
imagemin ${filename} --out-dir=optimized/compressed
# Generate responsive sizes
for size in 320 640 1024 1920; do
sharp resize \${size} --input ${filename} --output optimized/${name}-\${size}w.${ext}
sharp resize \${size} --input ${filename} --output optimized/webp/${name}-\${size}w.webp
done
`;
});
script += `
echo "Optimization complete!"
echo "Results saved in ./optimized/"
`;
return script;
}
// Generate Node.js optimization script
generateNodeScript(images) {
return `// Image Optimization Script
const sharp = require('sharp');
const imagemin = require('imagemin');
const imageminWebp = require('imagemin-webp');
const imageminAvif = require('imagemin-avif');
const fs = require('fs').promises;
const path = require('path');
async function optimizeImages() {
const images = ${JSON.stringify(images, null, 2)};
// Create output directories
await fs.mkdir('optimized/webp', { recursive: true });
await fs.mkdir('optimized/avif', { recursive: true });
for (const img of images) {
const filename = path.basename(img.url);
const name = path.parse(filename).name;
console.log(\`Processing \${filename}...\`);
try {
// Read original image
const buffer = await fs.readFile(filename);
// Generate WebP
await imagemin.buffer(buffer, {
plugins: [imageminWebp({ quality: 80 })]
}).then(data =>
fs.writeFile(\`optimized/webp/\${name}.webp\`, data)
);
// Generate AVIF
await imagemin.buffer(buffer, {
plugins: [imageminAvif({ quality: 80 })]
}).then(data =>
fs.writeFile(\`optimized/avif/\${name}.avif\`, data)
);
// Generate responsive sizes
for (const width of [320, 640, 1024, 1920]) {
await sharp(buffer)
.resize(width)
.toFile(\`optimized/\${name}-\${width}w.jpg\`);
await sharp(buffer)
.resize(width)
.webp({ quality: 80 })
.toFile(\`optimized/webp/\${name}-\${width}w.webp\`);
}
} catch (error) {
console.error(\`Failed to process \${filename}:\`, error);
}
}
console.log('Optimization complete!');
}
optimizeImages().catch(console.error);
`;
}
// Generate HTML updates
generateHTMLUpdates(images) {
let updates = `<!-- Image Optimization HTML Updates -->
<!-- Generated on ${new Date().toISOString()} -->
`;
images.forEach(img => {
const filename = img.url.split('/').pop();
const name = filename.substring(0, filename.lastIndexOf('.'));
// Skip if already optimized
if (img.element === 'source') return;
updates += `
<!-- Replace: <img src="${img.url}" ...> -->
<picture>
<source
type="image/avif"
srcset="${name}-320w.avif 320w,
${name}-640w.avif 640w,
${name}-1024w.avif 1024w,
${name}-1920w.avif 1920w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
>
<source
type="image/webp"
srcset="${name}-320w.webp 320w,
${name}-640w.webp 640w,
${name}-1024w.webp 1024w,
${name}-1920w.webp 1920w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
>
<img
src="${img.url}"
srcset="${name}-320w.jpg 320w,
${name}-640w.jpg 640w,
${name}-1024w.jpg 1024w,
${name}-1920w.jpg 1920w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
loading="lazy"
decoding="async"
alt="${img.hasAlt ? 'YOUR_ALT_TEXT' : 'ADD_DESCRIPTIVE_ALT_TEXT'}"
>
</picture>
`;
});
return updates;
}
}
// Generate optimization scripts
const automator = new ImageOptimizationAutomator();
automator.generateOptimizationScript().then(scripts => {
console.log('Bash Script:', scripts.bash);
console.log('Node Script:', scripts.node);
console.log('HTML Updates:', scripts.html);
});
Best Practices for Image Optimization
- Choose the Right Format: JPEG for photos, PNG for graphics with transparency
- Implement Modern Formats: Use WebP/AVIF with fallbacks
- Responsive Images: Serve different sizes for different screens
- Lazy Loading: Load images only when needed
- Optimize Compression: Balance quality vs file size
- Use CDN: Serve images from edge locations
- Enable Caching: Set proper cache headers
- Preload Critical Images: Use rel="preload" for hero images
- Monitor Performance: Track Core Web Vitals impact
- Automate Optimization: Include in build process
Common Image Optimization Mistakes
- Original Resolution: Serving 4K images for thumbnails
- Wrong Format: Using PNG for photographs
- No Compression: Serving unoptimized images from CMS
- Missing Alt Text: Accessibility and SEO impact
- Eager Loading Everything: Not using lazy loading
- Ignoring Modern Formats: Missing 30%+ size savings
- No Responsive Images: Same image for mobile and desktop
- Poor Caching: Re-downloading unchanged images
- JavaScript-Dependent Loading: Images that require JS
- Unoptimized Sprites: Huge sprite sheets with unused images
Remember: Images are often the largest performance bottleneck. A well-optimized image strategy can cut page load times in half and significantly improve user experience, especially on mobile devices.