Optimize images in your web app
40% of a web page's weight comes from images. 4 checkpoints to keep them from being your app's weakest link.
Performance is not a topic to optimize after the fact. It's a constraint to build in from the first mockups, especially for images. According to the Web Almanac 2024, they account for around 40% of the median page weight (900 KB out of 2.3 MB on mobile). And on mobile, 68% of pages have an image as their LCP element (Web Almanac 2024). The loading time your users feel is, 2 times out of 3, the loading time of an image.
Yet adoption is poor: only 42% of mobile pages use srcset, and only 55% of images have a non-empty alt attribute. Plenty of room to stand out.
Today, I'll share the method I apply on my projects to hunt down and fix badly optimized images.
Diagnose before you code
Before touching any code, measure. Two tools cover 95% of cases.
Lighthouse, run in private browsing (to avoid extension bias), gives you an overall score and points at the first culprits. The metric weights are:
| Metric | Weight | What it measures |
|---|---|---|
| TBT | 30% | Total main thread blocking time |
| LCP | 25% | Time until the largest element is visible |
| CLS | 25% | Visual stability during load |
| FCP | 10% | Time until the first content appears |
| SI | 10% | Perceived rendering speed |
đź’ˇ The Lighthouse Scoring Calculator lets you simulate the impact of an improvement before investing in it. Very useful for prioritization.
WebPageTest complements Lighthouse with a "waterfall" view of the load. That's where you identify exactly which image loads too early, too late, or from a server that's too slow.
4 questions to ask for every image
1. Is the image the right size?
Compression. Rule of thumb: JPEG quality at 75% is rarely distinguishable from the original by eye, for a file typically 3 to 4 times lighter. Two approaches:
- Lossless: strips metadata and redundant data without altering quality
- Lossy: reduces size by degrading quality in a controlled way
Format. Each format has its lane:
| Format | Use | Why |
|---|---|---|
| JPEG | Photos | Proven lossy compression, universal support |
| PNG | Graphics with transparency | Lossless, good for logos and screenshots |
| SVG | Icons, logos | Vector, scales without loss, very lightweight |
| WebP | Modern replacement | ~30% lighter than an equivalent JPEG |
| AVIF | Next-gen | Even lighter than WebP, slower to encode |
Inline Base64 only makes sense for tiny images (< 1 KB). Above that, the 33% encoding overhead cancels out any "fewer requests" gain.
Responsive. The classic mistake: loading the same 1200px-wide image on a 375px screen. srcset fixes it:
<img
srcset="image-400.webp 400w, image-800.webp 800w, image-1200.webp 1200w"
sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
src="image-1200.webp"
alt="Meaningful description for SEO and accessibility"
/>Configured properly, this easily cuts mobile image weight by a factor of 3.
2. Is the image loaded at the right time?
By default, the browser downloads every image as soon as the HTML is parsed. That's rarely what you want.
Lazy loading for everything below the fold:
<img src="image.jpg" loading="lazy" alt="..." />Explicit priority for LCP-critical images (hero, banner):
<link rel="preload" as="image" href="hero.webp" />
<img src="hero.webp" fetchpriority="high" alt="..." />For a carousel, only the first slide should be prioritized. The rest can, and should, wait:
<img src="slide-1.jpg" fetchpriority="high" alt="..." />
<img src="slide-2.jpg" fetchpriority="low" loading="lazy" alt="..." />
<img src="slide-3.jpg" fetchpriority="low" loading="lazy" alt="..." />3. Does the image arrive quickly for everyone?
Images are static assets. Two levers:
- Browser cache:
Cache-Control: public, max-age=15552000, immutable(180 days).immutableskips revalidation as long as the URL doesn't change. If you update an image, change its URL (a hash in the filename is the simplest pattern). - CDN for geographic distribution. Cloudflare, Vercel, TwicPics, Cloudinary, Imgix all do the job. The bonus with specialized services (TwicPics, Cloudinary): automatic generation of responsive variants and on-the-fly conversion to WebP/AVIF based on the visitor's browser.
4. Is the image referenced correctly?
Crawlers don't "see" images. They read their context. Two attributes do 90% of the work:
- Descriptive
alt(notimage1.jpg, and not empty either) - Readable filename (
sunset-beach-vacation.webprather thanIMG_1234.webp)
For important images (article illustrations, e-commerce products), a JSON-LD ImageObject strengthens the signal:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ImageObject",
"caption": "Sunset Beach Vacation",
"image": "https://example.com/media/sunset-beach-vacation.webp",
"description": "Sunset at the beach while on vacation"
}
</script>Let the framework do the dirty work
Everything above can be automated. The Image components in Next.js or NuxtImg handle, by default:
- on-the-fly WebP/AVIF conversion based on the browser
- responsive variant generation
- lazy loading (unless
priorityis set for the LCP image) - CLS prevention via mandatory
width/heightattributes
import Image from 'next/image';
<Image
src="/sunset.jpg"
alt="Sunset on the beach"
width={1200}
height={800}
priority // only for the LCP image
quality={75}
placeholder="blur"
/>;If you're on a modern framework, starting with this component removes most problems effortlessly. It's often the first thing I do when coming onto an existing project.
What I've learned
đź’ˇ Image optimization is one of the rare tasks where the effort is small and the measurable impact huge. Spending 2 hours auditing a site's images usually pays more than a week optimizing JavaScript.
And it's a task that keeps paying: every new image added without thought undoes part of the previous work. Better to enforce the right reflexes from day one. As with code quality, fixing it after the fact is far more expensive.