Modern Image Optimisation for Faster Web Pages
Most image optimisation advice stops at compression, which your build tool already handles. The decisions that actually move your LCP are elsewhere.
Almost every guide to image optimisation is really a guide to compression, and compression is the part of the problem that has been solved for a decade. Run the file through a decent encoder, pick a sensible quality setting, done. Your build tool probably does it already without being asked.
Meanwhile, the pages that feel slow are usually slow for reasons compression cannot touch. The hero image was lazy-loaded, so the browser deliberately waited before fetching the single most important byte on the page. The srcset was correct, but the sizes attribute lied, so a phone downloaded a 1600px file to display it at 380px. Nothing declared the dimensions, so the layout jumped twice while everything settled.
Those are the decisions worth spending attention on, and none of them is about file format. This is a practical pass through the ones that matter, roughly in order of how much they typically move the numbers.
What images actually cost you
Images are usually the largest thing on a page by byte weight, but weight is a proxy metric, not what users experience. Two measures matter more.
Largest Contentful Paint is very often an image. On a typical article or product page, the element that determines your LCP score is the hero, and everything that delays that one request delays the metric directly. This is why image work has outsized leverage on Core Web Vitals compared to, say, shaving JavaScript.
Cumulative Layout Shift is often caused by images too, especially when they arrive without the browser knowing how much room to reserve. The content reflows, someone loses their place or taps the wrong thing, and the page feels cheap regardless of how fast it technically loaded.
Keep both in mind, because several of the techniques below trade against each other. Aggressive lazy loading helps total bytes and hurts LCP. Heavy compression helps bytes and can hurt perceived quality on the one image people actually look at.
Formats: use AVIF, keep WebP as the fallback
The format question is more settled than it used to be.
AVIF should be your default for photographic content. It sits at roughly 93 to 94 per cent global browser support, having been in Chrome since 2020, Firefox since 2021 and Safari since version 16, and it delivers meaningfully smaller files than WebP at equivalent quality, with the gap widest at low and medium quality targets. It also handles transparency, HDR and wide colour gamut, which removes most reasons to reach for PNG.
WebP remains a sensible fallback, universally supported and comfortably better than JPEG. Serving AVIF with a WebP fallback and a JPEG at the end of the chain covers essentially everyone.
JPEG XL is technically excellent and still not a viable default. Chrome removed it in 2023, then re-added decoding behind a flag in Chrome 145 in early 2026 using a Rust decoder, but it is off by default there and in Firefox. Real-world support sits around 14 per cent, driven almost entirely by Safari. It is worth watching, particularly for archival and high-quality lossless work where it beats AVIF, but do not build a delivery pipeline on it yet.
For everything that isn't a photograph, the answer hasn't changed. SVG for logos, icons and illustrations, because it is resolution-independent and usually tiny once run through SVGO. PNG only for the narrow case of lossless raster with hard edges where SVG is not an option.
One easy win that still goes unclaimed: if you have any animated GIFs left, convert them to MP4 or WebM. The saving is routinely 90 per cent or more, because GIF was never an animation codec in any modern sense.
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="..." width="1600" height="900">
</picture>
Do not lazy-load your LCP image.
This is the single most common self-inflicted performance wound, and it usually comes from adding loading="lazy" globally as a site-wide default.
Native lazy loading is genuinely useful for images below the fold. Applied to the hero, it does the opposite of what you want: the browser defers a request it would otherwise have started immediately, and your LCP gets worse. Applying it everywhere is worse than applying it nowhere.
The correct pattern is the inverse for above-the-fold content:
<img src="hero.avif" alt="..." width="1600" height="900"
fetchpriority="high" decoding="async">
fetchpriority="high" tells the browser to promote this request ahead of the other things competing for bandwidth during initial load. On image-led pages, this alone often moves LCP by a few hundred milliseconds because the default heuristics don't know which image is important until layout has progressed.
If the hero is referenced from CSS rather than markup, or discovered late for any other reason, preload it in the head so the request starts during HTML parsing rather than after the stylesheet resolves:
<link rel="preload" as="image" href="hero.avif" fetchpriority="high">
Use preload sparingly. Preloading several images promotes all of them equally, which is the same as promoting none of them.
For everything below the fold, loading="lazy" is correct and costs nothing. The rough rule: first viewport eager and prioritised, everything after it lazy.
Responsive images, and the attribute everyone gets wrong
srcset is well understood. sizes is where responsive images quietly fail.
srcset tells the browser which files exist. sizes tells it how wide the image will actually be rendered, and the browser must decide which file to download before it has laid out the page, so it trusts your declaration completely. Get it wrong, and it will confidently fetch the wrong file, usually a much larger one.
<img src="photo-800.avif"
srcset="photo-400.avif 400w,
photo-800.avif 800w,
photo-1200.avif 1200w,
photo-1600.avif 1600w"
sizes="(min-width: 64rem) 40rem, 100vw"
alt="..." width="1600" height="900">
The common failure is leaving sizes at its default, which is 100vw, on an image that actually renders in a narrow column. Every device then downloads a file sized for the full viewport width. This is the bug that makes people conclude responsive images "did not help".
Two things make this less error-prone. For lazy-loaded images, sizes="auto" lets the browser use the real laid-out width, since by then layout has happened, which removes the guesswork entirely. And for anything with a fixed maximum column width, express sizes in the same units as your CSS so the two cannot drift apart.
On choosing breakpoints: generating widths at fixed device sizes is a habit worth dropping. Devices no longer cluster at predictable widths, and a component in a sidebar has nothing to do with the viewport. Generate variants at even byte-size intervals instead, something like a new step every 20 to 30 kB of encoded output, which produces more useful steps where the file size actually changes and fewer where it does not.
Use <picture> with media queries only for genuine art direction, meaning a different crop or composition at different sizes. If all you need is the same image at different resolutions, srcset on a plain <img> is simpler and works better.
Always declare dimensions
Every <img> should carry width and height attributes, or an aspect-ratio in CSS. This is the entire fix for image-caused layout shift; it takes seconds, and it is still missing from a large share of production sites.
The attributes do not force a size. Modern browsers use the ratio between them to reserve the correct space while the file downloads, and your CSS still controls actual rendered dimensions:
img {
max-inline-size: 100%;
block-size: auto;
}
Without that CSS pairing, the attributes will fight your layout. With it, you get reserved space and responsive behaviour together.
For images whose dimensions genuinely are not known until runtime, set aspect-ratio on the container and let the image fill it. A slightly wrong reserved box is still far better than no reserved box.
Let a pipeline do the work
Hand-optimising images does not survive contact with a real project. Someone will eventually upload a 4 MB PNG straight from a phone, and no amount of documented process prevents it.
An image CDN or build-time pipeline solves this structurally. Cloudinary, imgix, Netlify Image CDN, Vercel's image optimisation and the framework-level components built on them all do the same core job: accept one high-quality original, then derive format, dimensions and quality per request based on what the client actually supports. Content negotiation means a browser with AVIF support gets AVIF, and one without gets WebP, with no <picture> element to maintain.
If you would rather not add a dependency, sharp in a build step covers most of the same ground, generating your variant widths and formats from a source directory.
Either way, the principle is that quality settings and format decisions belong in configuration, applied uniformly, rather than in a checklist someone follows by hand. On quality, automatic modes are usually right: most CDNs offer a perceptual quality setting that analyses each image rather than applying a fixed number, which reliably beats a global quality value, since a flat photograph and a detailed one do not want the same setting.
Placeholders, and when they are worth it
Low-quality placeholders, blurred previews and dominant-colour fills are popular and frequently unnecessary.
They help in one specific case: a large image that genuinely takes a while to arrive, where showing something immediately makes the wait feel shorter. A blurred 20-byte preview under a hero can be worth it.
They hurt when applied indiscriminately. Every placeholder adds extra markup, extra bytes inlined into the HTML, and often extra JavaScript to swap in the real image. On a page with thirty thumbnails, that machinery costs more than it saves, and if your images are properly sized and prioritised, most of them arrive fast enough that there is nothing to disguise.
Fix the loading first. Add placeholders only where a measurable gap remains.
Alt text, properly
Alt text is an accessibility feature that also helps SEO, and treating it the other way round produces bad results on both counts.
Describe what the image shows, in the context of the surrounding content. A screen reader user has just heard the heading, so repeating the article title as alt text tells them nothing. Keyword stuffing is worse than useless: it makes the page harder to use, and search engines discount it anyway.
Purely decorative images should have an empty alt="" attribute, not a missing one and not a description. Empty alt tells assistive technology to skip the image, which is exactly right for a background flourish. Omitting the attribute entirely makes some screen readers announce the filename instead, which is the worst outcome available.
Charts, screenshots and diagrams need real description, and often more than an attribute comfortably holds. Put the short version in alt and the detail in surrounding prose or a caption, which benefits everyone rather than only the people who cannot see the image.
Measuring, so this does not regress
Lab tools tell you whether a change worked. Field data tells you whether it mattered.
Lighthouse and PageSpeed Insights are the obvious starting points, and their image audits are reliable: properly sized, next-gen formats, deferred offscreen images. Run them on throttled mobile rather than desktop, because that is where image weight actually bites.
WebPageTest's filmstrip view is more useful than a score when you are trying to work out why a page feels slow, since it shows the order things appear rather than a single number.
Chrome User Experience Report data shows what real visitors experience across the devices and connections they actually have, which is frequently much worse than anything you will reproduce locally.
Then hold the line automatically. A Lighthouse CI budget that fails a build when LCP regresses or when total image weight crosses a threshold catches the 4 MB PNG on the pull request rather than three weeks later.
The short version
If you do nothing else, do these four things, in this order.
Declare width and height on every image. Stop lazy-loading anything in the first viewport, and add fetchpriority="high" to the LCP image. Check that your sizes attribute reflects the real rendered width. Serve AVIF with a WebP fallback through an automated pipeline rather than by hand.
That covers most of the available gain. Compression settings, placeholder strategies and format edge cases are worth attention afterwards. Still, they are refinements on top of those four, and a page that gets the four right will beat a page with perfect encoding and a lazy-loaded hero every time.