Front-End · 10 min read · Updated

Image Optimisation for the Web: Formats, Sizes and Lazy Loading

Make images load fast: choose AVIF, WebP, JPEG or SVG, serve the right size with srcset, lazy-load below the fold and prioritise the LCP image.

Image optimisation: notepad Notebook
Photo: Negative Space via Stocksnap (CC0)

On most websites, images are the heaviest thing on the page. A single unoptimised photo straight from a phone camera can weigh more than all the HTML, CSS and JavaScript put together. On a mid-range phone with a patchy mobile connection – common for many visitors in India – that is the difference between a page that feels instant and one people leave.

Image optimisation is not one trick. It is four decisions: the right format, the right size, the right loading behaviour and enough information for the browser to lay out the page before the image arrives. This guide covers each with current browser support as of 2026.

Tip: Image optimisation directly affects two Core Web Vitals: Largest Contentful Paint (LCP), because the biggest element on screen is often an image, and Cumulative Layout Shift (CLS), when images without dimensions push content around. See Core Web Vitals for Designers.

1. Pick the right format

FormatBest forNotes
AVIFPhotos and complex imagesUsually the smallest files at similar quality. Supported in current versions of Chrome, Firefox, Safari and Edge (Edge was last, in version 121, early 2024). Encoding is slower.
WebPPhotos, graphics with transparencyMuch smaller than JPEG/PNG in most cases and supported by all modern browsers. A safe default.
JPEGPhotos, universal fallbackWorks everywhere. Use as the fallback inside <picture>.
PNGScreenshots, images needing lossless qualityLossless, so photos become very large. Often WebP lossless is smaller.
SVGLogos, icons, simple illustrationsVector: sharp at any size, often tiny, can be styled with CSS. Not for photos.
GIFAvoid for animationHuge files and limited colours. Use a muted, looping <video> instead.

A simple rule: SVG for anything drawn, AVIF or WebP for anything photographed, with JPEG as the fallback.

Serving modern formats with a fallback

The <picture> element lets the browser pick the first format it supports. Browsers that understand neither source use the <img> – which is also where alt, width and height belong.

<picture>
  <source srcset="/img/marina-beach.avif" type="image/avif">
  <source srcset="/img/marina-beach.webp" type="image/webp">
  <img src="/img/marina-beach.jpg" alt="Fishing boats on Marina Beach at sunrise"
       width="1200" height="800">
</picture>

AVIF now works in every major current browser, so some teams skip the fallback for new projects. Keeping a JPEG or WebP fallback is still sensible if your visitors include older phones that do not get browser updates.

2. Serve the right size

In image optimisation, format is only half the story. A perfectly compressed 3000px-wide image is still wasteful when it is shown 360px wide on a phone. Two attributes solve this:

  • srcset lists the available files and their real widths, using the w descriptor: tour-480.jpg 480w.
  • sizes tells the browser how wide the image will be displayed at different screen widths, so it can choose before the CSS has loaded.
<img
  src="/img/temple-800.jpg"
  srcset="/img/temple-480.jpg 480w,
          /img/temple-800.jpg 800w,
          /img/temple-1200.jpg 1200w,
          /img/temple-1600.jpg 1600w"
  sizes="(min-width: 1024px) 50vw, 100vw"
  alt="Gopuram of the Meenakshi Amman Temple, Madurai"
  width="1600" height="1067">

Read sizes as: “on screens 1024px and wider, this image fills half the viewport; otherwise it fills the full width”. The browser combines that with the screen’s pixel density and picks the smallest file that still looks sharp. A phone with a 2x screen showing the image at 400px wide may choose the 800w file.

Watch out: If you leave out sizes while using w descriptors, the browser assumes the image is 100vw wide. For a thumbnail in a three-column grid that means downloading a far larger file than needed. Match sizes to your real layout – see Responsive Design Breakpoints.

How many sizes should you make? Enough to avoid big jumps – often three to five widths between about 400px and twice your largest display width. CMSs help here: WordPress creates several sizes on upload and outputs srcset for images in content automatically.

3. Reserve space: width, height and aspect-ratio

Always put width and height attributes on images, using the file’s real pixel dimensions. Browsers use them to calculate the aspect ratio and reserve the right amount of space before the image downloads, so text does not jump down when it arrives. Your CSS can still make the image responsive:

img {
  max-width: 100%;
  height: auto; /* keeps the ratio from the width/height attributes */
}

/* Card thumbnails cropped to the same shape */
.card img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

object-fit: cover crops the image to fill its box without stretching, which keeps card grids tidy even when uploaded photos have different shapes. For more on the <img> element itself, see the HTML images lesson.

4. Control loading: lazy, eager and fetchpriority

loading=”lazy” for images below the fold

loading="lazy" tells the browser to wait until the image is getting close to the viewport before downloading it. It is supported in all major browsers – Safari added it in version 16.4 – and browsers that do not support it simply load the image normally. Chromium-based browsers start fetching well before the image is visible (web.dev describes thresholds of around 1,250px on fast connections and 2,500px on slow ones), so users rarely see an empty box.

Watch out: Do not lazy-load your hero image or anything visible on the first screen. The browser would delay the very image that decides your LCP. Lazy-load gallery items, cards further down, and images in long articles.

fetchpriority=”high” for the LCP image

The fetchpriority attribute takes high, low or auto. Adding fetchpriority="high" to the main hero image lets the browser fetch it early, instead of discovering its importance only after layout. It works in current Chrome, Edge, Safari (17.2+) and Firefox (132+). It is a hint, not a command, so use it on one or two images at most – if everything is high priority, nothing is.

<!-- First screen: load early -->
<img src="/img/hero-pondy-1200.webp" alt="Promenade Beach in Puducherry at dusk"
     width="1200" height="675" fetchpriority="high">

<!-- Further down the page: load when needed -->
<img src="/img/cafe-600.webp" alt="Bowl of idli and sambar at a White Town cafe"
     width="600" height="400" loading="lazy" decoding="async">

decoding="async" is a hint that the browser may decode the image without holding up other content. Its effect is usually small; formats and sizes matter far more.

CSS background images

Images set with background-image in CSS cannot use loading, srcset or alt, and the browser finds them late because it must download and parse the CSS first. If an image carries meaning or is your LCP element, use an <img>. Keep backgrounds for decoration, and use image-set() if you need different resolutions.

Try it: watch the browser choose from srcset

This demo draws two test images in JavaScript – one labelled 400w, one 800w – and offers both in srcset. The page reports which file the browser picked using currentSrc. Change the sizes value with the buttons and watch the choice change.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>srcset demo</title>
<style>
  body { font-family: system-ui, sans-serif; margin: 20px; }
  img { max-width: 100%; height: auto; display: block; border-radius: 8px; }
  .controls { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
  button { font: inherit; padding: 6px 12px; }
  #out { font-weight: 600; }
</style>
</head>
<body>
  <h1>Which file did the browser pick?</h1>
  <div class="controls">
    <button type="button" data-sizes="150px">sizes="150px"</button>
    <button type="button" data-sizes="300px">sizes="300px"</button>
    <button type="button" data-sizes="700px">sizes="700px"</button>
  </div>
  <p id="out">Loading…</p>
  <div id="holder"></div>
<script>
  function makeImage(width, colour, label) {
    const c = document.createElement('canvas');
    c.width = width; c.height = Math.round(width * 0.5);
    const ctx = c.getContext('2d');
    ctx.fillStyle = colour; ctx.fillRect(0, 0, c.width, c.height);
    ctx.fillStyle = '#ffffff';
    ctx.font = 'bold ' + Math.round(width / 8) + 'px sans-serif';
    ctx.fillText(label, width * 0.08, c.height * 0.62);
    return c.toDataURL('image/png');
  }
  const small = makeImage(400, '#0f766e', '400w file');
  const large = makeImage(800, '#1d4ed8', '800w file');
  const out = document.getElementById('out');
  const holder = document.getElementById('holder');

  function show(sizes) {
    const img = new Image();
    img.alt = 'Test image showing which file was chosen';
    img.width = 800; img.height = 400;
    img.sizes = sizes;
    img.srcset = small + ' 400w, ' + large + ' 800w';
    img.src = small;
    img.addEventListener('load', () => {
      const picked = img.currentSrc === large ? '800w' : '400w';
      out.textContent = 'sizes="' + sizes + '" at ' + window.devicePixelRatio + 'x pixel density: browser chose the ' + picked + ' file.';
    });
    holder.replaceChildren(img);
  }

  document.querySelectorAll('[data-sizes]').forEach(btn => {
    btn.addEventListener('click', () => show(btn.dataset.sizes));
  });
  show('300px');
</script>
</body>
</html>

On a standard 1x screen, sizes="300px" picks the 400w file; on a 2x or 3x screen the browser needs about 600 or 900 pixels, so it switches to the 800w file. Browsers may also keep a larger file they already have in cache, which is why results can differ after you click around. That is the whole idea: you describe the options, the browser decides.

5. Compress sensibly

  • Resize before compressing. Export at the largest size you actually display (times two for high-density screens), not the camera’s full resolution.
  • Use a visual check, not a fixed number. Lower the quality setting until you can see a difference at normal viewing size, then step back up a little. Faces and text in images need higher quality than textures.
  • Strip metadata. Camera data such as location and device details adds weight and can be a privacy risk.
  • Automate it. Browser-based tools such as Squoosh are fine for a few images; for many, use a build step, an image CDN or your CMS’s image processing so nobody has to remember.
  • Optimise SVGs too. Design tools often export extra metadata and hidden layers. An SVG optimiser can shrink them noticeably.

6. Do not forget alt text

Image optimisation is not only about speed. Every meaningful image needs alt text that describes its purpose – “Gopuram of the Meenakshi Amman Temple”, not “image1” or “photo”. Decorative images should have an empty alt="" so screen readers skip them. Good alt text also helps image search. See Web Accessibility Basics and Semantic HTML for SEO.

Pre-launch image optimisation checklist

  1. Logos and icons are SVG; photos are AVIF or WebP with a fallback where needed.
  2. No image is much larger than its largest displayed size (times screen density).
  3. Content images use srcset and an accurate sizes.
  4. Every <img> has width and height attributes.
  5. The hero or LCP image is not lazy-loaded and has fetchpriority="high".
  6. Images below the first screen use loading="lazy".
  7. Animated GIFs are replaced with short muted videos.
  8. Meaningful images have descriptive alt text; decorative ones have alt="".
  9. You tested on a real phone with throttled network in DevTools, and checked LCP and CLS in Lighthouse or PageSpeed Insights.

If you build on WordPress, many of these image optimisation steps can be handled by the platform and your theme – read How to Speed Up a WordPress Site Without Heavy Plugins. For the design side, agree image ratios and sizes in the design file early, so developers are not cropping by guesswork. For video and audio, see the HTML audio and video lesson, and for animation performance, CSS Animation Performance Tips.

Frequently asked questions

Should I use AVIF or WebP?

Both are good. AVIF usually gives smaller files at the same visual quality but takes longer to encode. A common approach is AVIF first and WebP or JPEG as fallbacks inside a picture element.

Is loading="lazy" supported in all browsers?

Yes, in current versions of all major browsers; Safari added image lazy loading in 16.4. Browsers that do not support it ignore the attribute and load images normally, so it is safe to use.

Should I lazy-load every image?

No. Never lazy-load images that are visible when the page first loads, especially the LCP image. Lazy-loading them delays the most important content.

Do I still need width and height if my CSS sets the width?

Yes. The attributes give the browser the aspect ratio before the image downloads, so it can reserve space and avoid layout shift. Add height: auto in CSS so the image still scales.

What does fetchpriority="high" do?

It hints to the browser that an image is important and should be fetched early. Use it on the main hero image only; marking many images as high priority removes the benefit.

Further reading

Keep reading

All articles