/ Image Optimisation Fixes / Image Size

How to Fix Image Size

Oversized images are the single biggest LCP killer on most sites. A 4MB hero photo displays at 800x600 because CSS scales it down — but the user still downloaded the full 4MB. Combined with mobile users on slow connections, this turns into a 5-second LCP and a poor Core Web Vitals score. The fix is straightforward: resize to display size, compress aggressively, deliver responsive variants. Done right, you cut image bytes 60-80% with no visible quality loss.

1. Audit oversized images

Step 1
Run the Image Optimisation audit
Findings ranked by waste: actual image dimensions vs displayed dimensions. A 4000x3000 image displayed at 800x600 wastes ~93% of its bytes.
Step 2
DevTools quick check
DevTools → right-click any image → "Inspect". Shows the natural dimensions (full image size) vs rendered dimensions (display size). Ratio over 1.5x means resizing helps.

2. Target dimensions

Goal: serve the smallest image that still looks crisp at the displayed size, accounting for device pixel ratio (DPR).

// Display size: 800x600 CSS pixels
// Standard display (1x DPR): serve 800x600
// Retina display (2x DPR): serve 1600x1200
// Modern phones (3x DPR): serve 2400x1800 (overkill, 2x usually fine)

// Rule of thumb: serve at 2x DPR for retina compatibility,
// don't waste bytes serving 3x — barely visible difference

3. Resize at upload or build

Sharp (Node.js)

const sharp = require('sharp');

async function generateVariants(input, basename) {
  const sizes = [400, 800, 1200, 1600, 2400];
  for (const width of sizes) {
    await sharp(input)
      .resize({ width, withoutEnlargement: true })
      .jpeg({ quality: 82, mozjpeg: true })
      .toFile(`./public/img/${basename}-${width}.jpg`);

    await sharp(input)
      .resize({ width, withoutEnlargement: true })
      .webp({ quality: 80 })
      .toFile(`./public/img/${basename}-${width}.webp`);
  }
}

WordPress (built-in)

WordPress generates multiple sizes automatically — thumbnail, medium, large, full. Configure additional sizes in functions.php:

add_image_size('hero-small', 400);
add_image_size('hero-medium', 800);
add_image_size('hero-large', 1200);
add_image_size('hero-xl', 1600);
add_image_size('hero-2x', 2400);

Image CDN

Cloudinary, ImageKit, Bunny CDN resize on the fly via URL parameters:

<img src="https://cdn.example.com/hero.jpg?w=800&q=82" alt="...">
<!-- Returns 800px-wide JPEG at quality 82 -->

4. Compress aggressively

JPEG quality sweet spot

// Sharp
sharp(input).jpeg({ quality: 82, mozjpeg: true });

// mozjpeg encoder produces smaller files than libjpeg at same quality
// quality 82 visually indistinguishable from 100 for most photos
// quality 100 typically 40-60% larger for no visible benefit

PNG: lossy when transparency allows

# pngquant for lossy PNG-8
pngquant --quality=70-90 --strip image.png -o image-optimised.png

# Typically 60-80% size reduction
# Visible quality loss minimal for photos
# Keeps transparency unlike JPEG conversion

Strip metadata

// Removes EXIF, ICC profile, GPS data
// Saves bytes AND privacy (no GPS in publicly shared photos)
sharp(input).jpeg({ quality: 82 }).toFile(output);
// Sharp strips by default — verify with `exiftool output.jpg`

5. Serve responsive variants with srcset

<img src="/img/hero-1200.jpg"
     srcset="/img/hero-400.jpg 400w,
             /img/hero-800.jpg 800w,
             /img/hero-1200.jpg 1200w,
             /img/hero-1600.jpg 1600w,
             /img/hero-2400.jpg 2400w"
     sizes="(max-width: 600px) 100vw,
            (max-width: 1200px) 50vw,
            1200px"
     alt="Description"
     width="1200" height="675"
     loading="lazy">

Understanding sizes

The sizes attribute tells the browser what size the image will be displayed at, based on viewport. Browser then picks the smallest srcset variant that's still big enough.

Combine with picture for format fallback

<picture>
  <source type="image/avif" srcset="/img/hero-400.avif 400w, /img/hero-800.avif 800w,
                                     /img/hero-1200.avif 1200w, /img/hero-2400.avif 2400w"
          sizes="(max-width: 800px) 100vw, 1200px">
  <source type="image/webp" srcset="/img/hero-400.webp 400w, /img/hero-800.webp 800w,
                                     /img/hero-1200.webp 1200w, /img/hero-2400.webp 2400w"
          sizes="(max-width: 800px) 100vw, 1200px">
  <img src="/img/hero-1200.jpg"
       srcset="/img/hero-400.jpg 400w, /img/hero-800.jpg 800w,
               /img/hero-1200.jpg 1200w, /img/hero-2400.jpg 2400w"
       sizes="(max-width: 800px) 100vw, 1200px"
       width="1200" height="675"
       alt="Description"
       loading="lazy">
</picture>

6. Special-case the LCP image

The LCP (Largest Contentful Paint) image — usually the hero — gets priority:

<img src="/img/hero-1200.jpg"
     srcset="..."
     sizes="..."
     width="1200" height="675"
     alt="..."
     fetchpriority="high"
     loading="eager">

7. Below-fold images: lazy load

<img src="/img/gallery-item.jpg"
     loading="lazy"
     decoding="async"
     width="400" height="300"
     alt="...">

Browser only downloads when image is near viewport. Most images on a page should have loading="lazy" — except the LCP image and any image visible before scrolling.

8. Build pipeline integration

Next.js Image (handles most of this automatically)

import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="..."
  width={1200}
  height={675}
  priority  // for LCP image
  sizes="(max-width: 800px) 100vw, 1200px"
/>
// Generates AVIF, WebP, multiple sizes, sets srcset, lazy by default

Astro / Vite image plugins

// Astro
import { Image } from 'astro:assets';
<Image src={heroImage} alt="..." widths={[400, 800, 1200]} formats={['avif', 'webp', 'jpg']} />

9. Verify byte savings

Step 1
Total page weight before vs after
DevTools → Network tab → reload → bottom right shows total transferred bytes. Should drop 40-70% on image-heavy pages.
Step 2
Lighthouse LCP improvement
Mobile Lighthouse pre-fix vs post-fix. Heroes dropping from 4MB to 250KB typically improve LCP by 1.5-3 seconds on 3G/4G connections.
💡 The 80/20 of image-size fixes: identify your 5 biggest images by byte size, optimise them, redeploy. That covers 70-80% of total image weight on most sites. The long tail of small icons can wait.

🖼️ Re-run the Image Optimisation audit

Verify image-size findings are cleared and measure byte savings.

Run Image Audit →
Related Guides: Image Optimisation Fixes  ·  Fix Image Format  ·  Fix Image Lazy Load  ·  Image Optimisation Guide
💬 Got a problem?