Performance Techniques for Modern Web Applications
The Problem: Images account for 50-90% of total page weight. Unoptimized images destroy performance, kill SEO, and frustrate users. Yet most developers treat image optimization as an afterthought.
The Solution: A comprehensive image optimization strategy that combines format selection, compression techniques, responsive delivery, and lazy loading. This article covers the complete toolkit for making images blazing fast.
// Picture element with format fallback
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description" loading="lazy">
</picture>
// Image optimization utility
class ImageOptimizer {
constructor() {
this.qualities = {
avif: 0.7,
webp: 0.8,
jpeg: 0.85,
png: 0.9
};
}
async optimize(file, options = {}) {
const {
maxWidth = 1920,
maxHeight = 1080,
format = 'webp'
} = options;
const img = await this.loadImage(file);
const canvas = this.resize(img, maxWidth, maxHeight);
// Convert to desired format
const blob = await this.canvasToBlob(canvas, format);
return new File([blob], file.name.replace(/\.[^.]+$/, `.${format}`), {
type: `image/${format}`
});
}
loadImage(file) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
}
resize(img, maxWidth, maxHeight) {
const canvas = document.createElement('canvas');
let width = img.width;
let height = img.height;
// Calculate dimensions
if (width > maxWidth) {
height = (maxWidth / width) * height;
width = maxWidth;
}
if (height > maxHeight) {
width = (maxHeight / height) * width;
height = maxHeight;
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
return canvas;
}
canvasToBlob(canvas, format) {
return new Promise((resolve) => {
canvas.toBlob(resolve, `image/${format}`, this.qualities[format]);
});
}
// Generate multiple formats
async generateFormats(file) {
const formats = ['avif', 'webp', 'jpeg'];
const results = {};
for (const format of formats) {
try {
results[format] = await this.optimize(file, { format });
} catch (error) {
console.warn(`Failed to generate ${format}:`, error);
}
}
return results;
}
}
// Usage
const optimizer = new ImageOptimizer();
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
const formats = await optimizer.generateFormats(file);
console.log('Generated formats:', formats);
// Upload all formats to server
});
// Generate responsive image sizes
async function generateResponsiveSizes(file) {
const sizes = [320, 640, 960, 1280, 1920];
const optimizer = new ImageOptimizer();
const results = {};
for (const size of sizes) {
const optimized = await optimizer.optimize(file, {
maxWidth: size,
format: 'webp'
});
results[size] = optimized;
}
return results;
}
// Generate srcset
function generateSrcSet(sizes, baseUrl) {
return Object.entries(sizes)
.map(([size, file]) => `${baseUrl}/${file.name} ${size}w`)
.join(', ');
}
// Usage in HTML
const sizes = await generateResponsiveSizes(file);
const srcset = generateSrcSet(sizes, '/images');
<img srcset="${srcset}" sizes="(max-width: 768px) 100vw, 50vw" src="/images/image-1920.webp" alt="Responsive image">
// Intersection Observer for lazy loading
class LazyLoader {
constructor(options = {}) {
this.options = {
rootMargin: '50px',
threshold: 0.01,
...options
};
this.observer = new IntersectionObserver(
this.onIntersect.bind(this),
this.options
);
}
observe(element) {
this.observer.observe(element);
}
onIntersect(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
this.loadImage(img);
this.observer.unobserve(img);
}
});
}
loadImage(img) {
const src = img.dataset.src;
const srcset = img.dataset.srcset;
if (src) img.src = src;
if (srcset) img.srcset = srcset;
img.classList.add('loaded');
}
}
// Initialize
const lazyLoader = new LazyLoader();
document.querySelectorAll('img[data-src]').forEach(img => {
lazyLoader.observe(img);
});
// Progressive image loading
class ProgressiveLoader {
async load(imgElement) {
const lowQualitySrc = imgElement.dataset.srcLow;
const highQualitySrc = imgElement.dataset.srcHigh;
// Load low quality first
const lowQualityImg = new Image();
lowQualityImg.src = lowQualitySrc;
await new Promise(resolve => {
lowQualityImg.onload = resolve;
});
imgElement.src = lowQualitySrc;
imgElement.classList.add('low-quality');
// Load high quality in background
const highQualityImg = new Image();
highQualityImg.src = highQualitySrc;
await new Promise(resolve => {
highQualityImg.onload = resolve;
});
// Fade in high quality
imgElement.style.transition = 'opacity 0.3s ease';
imgElement.style.opacity = '0';
setTimeout(() => {
imgElement.src = highQualitySrc;
imgElement.classList.remove('low-quality');
imgElement.style.opacity = '1';
}, 300);
}
}
// Server-side image optimization with Sharp
const sharp = require('sharp');
const fs = require('fs').promises;
async function optimizeImage(inputPath, outputPath) {
await sharp(inputPath)
.resize(1920, 1080, {
fit: 'inside',
withoutEnlargement: true
})
.webp({ quality: 80 })
.toFile(outputPath);
}
async function generateResponsiveVariants(inputPath, outputDir) {
const sizes = [320, 640, 960, 1280, 1920];
const results = [];
for (const size of sizes) {
const outputPath = `${outputDir}/image-${size}.webp`;
await sharp(inputPath)
.resize(size, null, {
fit: 'inside',
withoutEnlargement: true
})
.webp({ quality: 80 })
.toFile(outputPath);
results.push({
size,
path: outputPath,
stats: await fs.stat(outputPath)
});
}
return results;
}
// Real-time URL-based optimization
// ImageKit example
const base = 'https://ik.imagekit.io/your-id/';
function optimizedUrl(path, options = {}) {
const {
width = 1920,
height = 1080,
format = 'webp',
quality = 80,
auto = 'format' // Auto-select best format
} = options;
const params = new URLSearchParams({
tr: `w-${width},h-${height},f-${format},q-${quality},pr-true`
});
return `${base}${path}?${params}`;
}
// Usage
const heroUrl = optimizedUrl('/hero-image.jpg', {
width: 1920,
height: 1080,
quality: 85
});
<img src="${heroUrl}" alt="Hero image">
// Track image performance
class ImageMetrics {
constructor() {
this.metrics = [];
}
track(img) {
const startTime = performance.now();
img.addEventListener('load', () => {
const loadTime = performance.now() - startTime;
const size = this.getImageSize(img);
this.metrics.push({
src: img.src,
loadTime,
size,
dimensions: {
width: img.naturalWidth,
height: img.naturalHeight
}
});
this.analyze();
});
}
getImageSize(img) {
// Get actual transferred size from Resource Timing API
const entries = performance.getEntriesByName(img.src);
if (entries.length > 0) {
return entries[0].transferSize;
}
return null;
}
analyze() {
const avgLoadTime = this.metrics.reduce((sum, m) =>
sum + m.loadTime, 0) / this.metrics.length;
const avgSize = this.metrics.reduce((sum, m) =>
sum + (m.size || 0), 0) / this.metrics.length;
console.log('Average image load time:', avgLoadTime.toFixed(2), 'ms');
console.log('Average image size:', (avgSize / 1024).toFixed(2), 'KB');
}
}
// Initialize
const metrics = new ImageMetrics();
document.querySelectorAll('img').forEach(img => metrics.track(img));
Implementing these techniques on a production site:
Image optimization isn't optional—it's essential. Modern formats, smart compression, responsive delivery, and lazy loading can reduce image weight by 90%+ while maintaining visual quality.
The key is automation. Build optimization into your upload pipeline, use format fallbacks, and always size for the viewport. Your users (and your SEO) will thank you.
Fast images aren't a luxury—they're a requirement for modern web performance.