Your images are probably the problem
You know that moment when you run Lighthouse and it screams at you about image sizes? Yeah. Let's fix that.
Images account for 40-60% of most pages' total weight. On e-commerce sites or portfolios, it's often north of 80%. Which means image optimization is almost certainly the single biggest performance win sitting on the table for your site right now.
I once shaved 2 seconds off LCP just by converting one hero image to WebP and adding a preload tag. Two seconds. One image. That's the kind of low-hanging fruit we're talking about.
How images tank your Core Web Vitals
Two of the three Core Web Vitals are directly affected by images, and Google is ranking you on these.
Largest Contentful Paint (LCP) measures when your biggest visible element finishes rendering. On most pages, that's a hero image. Google wants this under 2.5 seconds, and an unoptimized 3 MB JPEG hero is the fastest way to blow past that.
Here's the checklist I run through for every LCP image:
- Compress hard (quality 70-80% — you won't see the difference, I promise)
- Use WebP or AVIF for 30-50% smaller files
- Resize to actual display dimensions, not the 4000px original from the camera
- Preload it with
<link rel="preload"> - Do not lazy-load it — it needs to load immediately
Cumulative Layout Shift (CLS) is the other one. If you've ever been reading an article and the text jumps because an image loaded above it, that's CLS. Target is under 0.1.
The fix is dead simple: always set width and height on your <img> elements, or use aspect-ratio in CSS. That's it. The browser reserves space before the image loads and nothing shifts.
Pick the right format (it matters more than you think)
Format selection is the highest-ROI optimization you can make. (I go deep on this in my PNG vs JPG vs WebP comparison and the WebP vs AVIF breakdown.) Here's what I reach for:
| Content Type | My go-to | Fallback |
|---|---|---|
| Photographs | AVIF, then WebP | JPEG |
| Screenshots/UI | WebP lossless | PNG |
| Icons/logos | SVG | WebP lossless |
| Animations | WebP animated | GIF (avoid if you can) |
Here's the <picture> element I use on basically every project now:
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image" width="1200" height="630">
</picture>
The browser picks the first format it supports. AVIF-capable browsers get the smallest file, everyone else gets WebP, and that one person on IE 11 gets the JPEG. Everybody wins.
Responsive images (stop serving 2400px to phones)
This one drives me nuts when I see it in production. A 2400px hero image being served to a 375px phone screen. That's easily 5x more data than needed on the connection that can least afford it.
<img
srcset="photo-400.webp 400w,
photo-800.webp 800w,
photo-1200.webp 1200w,
photo-1600.webp 1600w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
800px"
src="photo-800.webp"
alt="Description"
width="1600"
height="900"
>
I generate at 400, 800, 1200, and 1600px widths. Covers basically everything without creating a ridiculous number of variants.
Lazy loading (but not everywhere)
Anything below the fold gets loading="lazy":
<img src="photo.webp" loading="lazy" alt="Description" width="800" height="600">
Two rules though: never lazy-load your LCP image, and never lazy-load anything visible in the initial viewport. I've seen people slap loading="lazy" on every image including the hero and then wonder why their LCP tanked. Don't be that person.
Quality settings I actually use
After optimizing probably thousands of images at this point, here's where I land on quality settings:
WebP — I default to 75. It's the sweet spot. Go 85 if the client is pixel-peeping, 60 for thumbnails nobody's zooming into.
AVIF — AVIF punches way above its weight at lower quality values. 55 gets you roughly the same visual quality as WebP 75, but smaller. I start at 55 and adjust from there.
JPEG (MozJPEG) — If you're still serving JPEG (sometimes you have to), MozJPEG at 78 is a solid default. Noticeably smaller than standard JPEG encoding at the same quality.
You can dial these in with CanYouSmoosh if you want to see exactly what a given quality level looks like on your specific images before committing to it.
Framework opinions
Next.js handles this best out of the box. The <Image> component does format negotiation, responsive sizing, and lazy loading automatically. If you're already on Next, just use it.
Astro's solid too — the @astrojs/image integration is well-designed and the output is clean.
If you're on Gatsby in 2026... my condolences. gatsby-plugin-image works, but you're fighting the framework at this point.
For everything else, sharp in Node does the job:
import sharp from 'sharp';
await sharp('input.jpg')
.resize(1200)
.webp({ quality: 75 })
.toFile('output.webp');
For images that don't change between builds — hero images, product photos, team headshots — I prefer to optimize them once locally and commit the results. CanYouSmoosh is great for this workflow. Compress locally, eyeball the quality, commit the optimized versions. No build-time processing needed.
CDN and caching (the boring stuff that matters)
Once your images are optimized, set aggressive cache headers:
Cache-Control: public, max-age=31536000, immutable
Use content hashes in filenames (hero-a3b4c5.webp) so you can cache-bust when images change without clearing the whole cache.
Most modern CDNs — Cloudflare, Vercel, CloudFront — can do format negotiation automatically, serving AVIF or WebP based on the Accept header. If your CDN supports this, you might not even need the <picture> element. Worth checking.
Here's how I check if it actually worked
I don't run a formal "measurement protocol" or whatever. I just:
- Lighthouse in Chrome DevTools — quick gut check on the performance score. Did the number go up? Good.
- DevTools Network tab — filter by images, sort by size. Your total image weight on a typical page should be under 500 KB. If it's not, you've got work to do.
- PageSpeed Insights — this one shows real user data (field data), not just lab results. Your LCP might look fine on your MacBook Pro on gigabit fiber. Check what it looks like for real users.
- WebPageTest — when I need the full waterfall to figure out why one specific image is loading late.
The numbers I care about: LCP under 2.5s, total image weight under 500 KB, CLS under 0.1.
If you only do six things, do these
Short on time? These will get you 80% of the way there in an afternoon:
- Convert your JPEG/PNG images to WebP (25-35% savings for free)
- Add
widthandheightto every<img>tag (goodbye, CLS) - Add
loading="lazy"to below-fold images - Preload your hero/LCP image
- Resize images to their actual display dimensions
- Set long cache headers on image assets
Each one takes minutes. Collectively they'll move your Lighthouse score more than most "performance optimization" refactors that take weeks.
Image optimization isn't sexy work, but it's the kind of thing that compounds. Better scores, faster loads, happier users, better SEO. And honestly, most of it is a one-time setup. Pick your formats, set your quality defaults, wire up responsive images, and move on. For the compression step, CanYouSmoosh handles it in seconds — drop your images in, dial in your settings, and you're done.
And if privacy matters to you (it should), check out why browser-based image compression is more private than uploading to some random server.
Ship it and go build something interesting.