Featured

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.

TechWebPerformance

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:

MetricWeightWhat it measures
TBT30%Total main thread blocking time
LCP25%Time until the largest element is visible
CLS25%Visual stability during load
FCP10%Time until the first content appears
SI10%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:

FormatUseWhy
JPEGPhotosProven lossy compression, universal support
PNGGraphics with transparencyLossless, good for logos and screenshots
SVGIcons, logosVector, scales without loss, very lightweight
WebPModern replacement~30% lighter than an equivalent JPEG
AVIFNext-genEven 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). immutable skips 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 (not image1.jpg, and not empty either)
  • Readable filename (sunset-beach-vacation.webp rather than IMG_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 priority is set for the LCP image)
  • CLS prevention via mandatory width / height attributes
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.