How to Optimize Images for Web Performance
Last month, I was helping a friend debug why their portfolio site felt sluggish. The culprit? A 4MB hero image that took 8 seconds to load on mobile. Their beautiful photography was driving visitors away before they could even see it.
Images often make up 50-70% of a page's total size. Mess them up, and you're killing your site's speed. Nail them, and you'll boost load times, SEO rankings, and user happiness.
Let's walk through everything you need to know to optimize images like a pro.
Choosing the Right Image Format
The format wars have settled, and we have clear winners for different use cases:
WebP should be your default choice. It offers 25-35% smaller file sizes compared to JPEG at equivalent quality, supports transparency, and has near-universal browser support. Unless you're targeting IE11 users (please don't), WebP is your friend.
AVIF is the new kid that packs an even bigger punch—up to 50% smaller than JPEG. Browser support is strong in modern browsers, but check CanIUse for your specific audience before going all-in. It's worth considering as a progressive enhancement.
PNG still has its place for graphics with sharp edges, text, or when you need lossless quality. But for photographs? Skip it.
SVG is perfect for icons, logos, and simple illustrations. They scale infinitely and are often tiny in file size.
Tools like Sharp make conversion easy—here's a quick Node.js script to convert images to WebP:
import sharp from 'sharp'
async function convertToWebP(inputPath: string, outputPath: string) {
try {
await sharp(inputPath)
.webp({ quality: 80 })
.toFile(outputPath)
console.log(`Converted ${inputPath} to WebP`)
} catch (err) {
console.error('Conversion failed:', err)
}
}
// Convert a single image
convertToWebP('./hero-image.jpg', './hero-image.webp')Note: These examples use Node.js and Sharp—install via
npm install sharp.
Compression: Finding the Sweet Spot
Here's a confession: I used to upload images straight from my camera to production. 6000x4000 pixels, minimal compression, the works. Don't be like past me.
The goal is finding the quality threshold where images still look great but file sizes are reasonable. For most web images, a quality setting between 75-85 produces visually indistinguishable results from the original.
Here's a more complete optimization script:
import sharp from 'sharp'
import fs from 'fs/promises'
interface OptimizeOptions {
width?: number
quality?: number
}
async function optimizeImage(
inputBuffer: Buffer,
options: OptimizeOptions = {}
): Promise<Buffer> {
const { width = 1920, quality = 80 } = options
try {
return await sharp(inputBuffer)
.resize(width, null, {
withoutEnlargement: true,
fit: 'inside'
})
.webp({ quality })
.toBuffer()
} catch (err) {
console.error('Optimization failed:', err)
throw err
}
}
// Example usage
const originalImage = await fs.readFile('./massive-photo.jpg')
const optimizedImage = await optimizeImage(originalImage, {
width: 1200,
quality: 82
})
console.log(`Original: ${(originalImage.length / 1024).toFixed(1)}KB`)
console.log(`Optimized: ${(optimizedImage.length / 1024).toFixed(1)}KB`)Pro tip: Always set a maximum width. Nobody needs a 4000px wide image on a webpage. For most use cases, 1920px is plenty for full-width images, and 800-1200px works great for content images.
Responsive Images: Serve What's Needed
Why send a 1920px image to someone browsing on their phone? The srcset attribute lets browsers choose the most appropriate image size:
<img
src="https://cdn.easycdn.co/677bef021dd44feb8fbfc21a/b8c3e2a1-5d4f-4a2b-9c1e-3f6a8b7c9d0e.webp"
srcset="
https://cdn.easycdn.co/677bef021dd44feb8fbfc21a/a1b2c3d4-1a2b-3c4d-5e6f-7a8b9c0d1e2f.webp 400w,
https://cdn.easycdn.co/677bef021dd44feb8fbfc21a/b8c3e2a1-5d4f-4a2b-9c1e-3f6a8b7c9d0e.webp 800w,
https://cdn.easycdn.co/677bef021dd44feb8fbfc21a/c9d4f3b2-6e5g-5b3c-0d2f-4g9c0d2e3f4g.webp 1200w,
https://cdn.easycdn.co/677bef021dd44feb8fbfc21a/d0e5g4c3-7f6h-6c4d-1e3g-5h0d1e3f4g5h.webp 1920w
"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
alt="A descriptive alt text"
loading="lazy"
/>This tells the browser: "Here are multiple sizes, pick the one that makes sense for this device." A phone on a slow connection will grab the 400px version while a desktop with a retina display might fetch the 1920px one.
Lazy Loading: Load Images When They're Needed
Remember when we had to write custom JavaScript or use libraries for lazy loading? Those days are gone. Native lazy loading is now supported everywhere that matters:
<img src="image.webp" alt="Description" loading="lazy" />That's it. One attribute. The browser handles the rest, loading images only as they approach the viewport.
For above-the-fold images (your hero, logo, etc.), skip lazy loading entirely. You want those to load immediately:
<!-- Hero image - load immediately -->
<img src="hero.webp" alt="Hero" loading="eager" fetchpriority="high" />
<!-- Content images - lazy load -->
<img src="content-1.webp" alt="Content" loading="lazy" />
<img src="content-2.webp" alt="Content" loading="lazy" />The fetchpriority="high" attribute tells the browser this image is critical and should be prioritized in the loading queue.
The CDN Advantage
All of this optimization work means nothing if your images are served from a slow server halfway around the world. A CDN caches your images on edge servers globally, so users download from a location near them.
The difference is dramatic. I've seen image load times drop from 2+ seconds to under 200ms just by moving to a CDN—that's not a marginal improvement, it's transformative for user experience.
Here's what a typical upload flow might look like with easyCDN:
import { createClient } from '@easycdn/server'
const cdn = createClient({
secretKey: process.env.EASYCDN_SECRET_KEY!,
})
async function uploadOptimizedImage(inputBuffer: Buffer, filename: string) {
try {
const result = await cdn.upload(inputBuffer, {
fileName: filename,
transform: {
image: {
width: 1920,
format: 'webp',
quality: 82,
},
},
})
return result.asset.url
} catch (err) {
console.error('Upload failed:', err)
throw err
}
}Quick Wins Checklist
Before you go, here's a checklist you can run through for any project:
- Convert images to WebP (or AVIF for progressive enhancement)
- Resize images to maximum display dimensions
- Compress with quality 75-85
- Implement responsive images with
srcset - Add
loading="lazy"to below-fold images - Use
fetchpriority="high"on critical images - Serve images from a CDN
- Test with Lighthouse or WebPageTest
Wrapping Up
Remember my friend's sluggish portfolio site? After implementing these optimizations, it loaded in under 2 seconds—yours can too.
Image optimization isn't glamorous, but it's one of the highest-impact performance improvements you can make. A few hours of work can shave seconds off your load times and dramatically improve the experience for every visitor. Run your site through Lighthouse after implementing these changes—you'll likely see your Performance score jump by 20-30 points.
As an indie hacker, I know you'd rather code features than wrangle infrastructure. That's why easyCDN handles uploads, optimization, and global caching for you. Upload your images, and we'll serve them fast from edge locations worldwide. It's built specifically for developers who want to ship, not configure.
Your users (and your Lighthouse score) will thank you.
