How to reduce image sizes by up to 90% without visible quality loss — covering modern formats, responsive loading, and automated pipelines.
Images account for approximately 50% of the average web page's total weight. A single unoptimized photograph can be 5-10MB straight from a camera, while the same image optimized for web can be 100-200KB with no perceptible quality difference.
The impact is direct:
The most efficient format available today:
Google's format that's now universally supported:
The picture element lets you serve different formats and sizes based on the browser's capabilities:
<picture>
<source
type="image/avif"
srcset="hero-400.avif 400w,
hero-800.avif 800w,
hero-1200.avif 1200w"
sizes="(max-width: 640px) 100vw,
(max-width: 1024px) 80vw,
60vw"
/>
<source
type="image/webp"
srcset="hero-400.webp 400w,
hero-800.webp 800w,
hero-1200.webp 1200w"
sizes="(max-width: 640px) 100vw,
(max-width: 1024px) 80vw,
60vw"
/>
<img
src="hero-800.jpg"
alt="Descriptive alt text"
width="1200"
height="675"
loading="lazy"
decoding="async"
/>
</picture>
For a Vite-based project, you can automate image optimization during build:
// vite.config.js
import { defineConfig } from 'vite'
import imagemin from 'vite-plugin-imagemin'
export default defineConfig({
plugins: [
imagemin({
gifsicle: { optimizationLevel: 3 },
mozjpeg: { quality: 80 },
pngquant: { quality: [0.7, 0.85] },
webp: { quality: 80 },
avif: { quality: 50, speed: 4 }
})
]
})
For existing image assets, a Node.js script using sharp can process entire directories:
import sharp from 'sharp'
import { readdir } from 'fs/promises'
import { join, parse } from 'path'
const INPUT_DIR = './public/assets/images'
const OUTPUT_DIR = './public/assets/optimized'
const SIZES = [400, 800, 1200]
async function processImages() {
const files = await readdir(INPUT_DIR)
const images = files.filter(f =>
/\.(jpg|jpeg|png)$/i.test(f)
)
for (const file of images) {
const { name } = parse(file)
const input = join(INPUT_DIR, file)
for (const width of SIZES) {
const base = sharp(input).resize(width)
await base.clone()
.avif({ quality: 50 })
.toFile(join(OUTPUT_DIR, ${name}-${width}.avif))
await base.clone()
.webp({ quality: 80 })
.toFile(join(OUTPUT_DIR, ${name}-${width}.webp))
await base.clone()
.jpeg({ quality: 82, mozjpeg: true })
.toFile(join(OUTPUT_DIR, ${name}-${width}.jpg))
}
}
}
processImages()
The simplest approach — just add loading="lazy" to any image below the fold:
<img src="photo.jpg" alt="..." loading="lazy" />
For more control over when images load and with loading animations:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
img.classList.add('loaded')
observer.unobserve(img)
}
})
}, { rootMargin: '200px' })
document.querySelectorAll('img[data-src]')
.forEach(img => observer.observe(img))
The rootMargin: '200px' starts loading images 200px before they enter the viewport, ensuring a smooth experience.
After optimization, measure the impact:
Metric | Before | After | Improvement
--- | --- | --- | ---
Total image weight | 8.2 MB | 890 KB | 89% reduction
LCP (mobile) | 4.2s | 1.8s | 57% faster
Page load time | 6.1s | 2.3s | 62% faster
Lighthouse Performance | 54 | 92 | +38 points
Image optimization is one of the highest-impact performance improvements you can make. The modern web has excellent format support (AVIF, WebP), native lazy loading, and powerful build tools. Implementing even a few of these techniques can dramatically improve your site's speed, user experience, and search ranking.