Image Optimization
Images are typically the largest assets on a page — choosing the right format, size, and loading strategy can halve load time without changing a pixel of visible content.
- Select the right image format for photographs, transparent graphics, and cutting-edge delivery
- Use the picture element to serve AVIF with WebP and JPEG fallbacks
- Apply lazy loading and fetchpriority to control download timing
- Prevent CLS by always reserving space for images before they load
On a typical webpage, images account for more than half of the total bytes transferred over the network. Unlike JavaScript, they run no code; unlike CSS, they affect no layout logic. Yet they are consistently the single biggest contributor to a slow LCP. The good news is that image optimisation is almost entirely a build-time concern — the user pays the cost of a bad decision, but the developer pays no runtime complexity to fix it.
Format selection
Choosing the right container format is the highest-leverage decision. Four formats dominate the web:
| Format | Compression | Transparency | Use when |
|---|---|---|---|
| JPEG | Lossy | No | Photographs |
| PNG | Lossless | Yes | Icons, screenshots, line art |
| WebP | Both modes | Yes | Most images — better than JPEG/PNG at same quality |
| AVIF | Lossy (primarily) | Yes | Best available compression; 2023+ browser support |
JPEG is the baseline for photographs. WebP delivers 25–35% smaller files than JPEG at equivalent visual quality and has near-universal browser support (96%+ as of 2025). AVIF goes further — another 20–30% smaller than WebP — but still has gaps in older browsers. The practical solution is to offer both and let the browser choose.
The <picture> element for format fallbacks
The <picture> element wraps multiple <source> alternatives. The browser
evaluates them top to bottom and picks the first type it supports:
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="A mountain at sunrise" width="1200" height="800">
</picture>A browser that understands AVIF downloads hero.avif and ignores the rest. A
browser that only understands WebP skips the AVIF source and downloads
hero.webp. Every browser understands JPEG, so the <img> src is the final
fallback. The user always gets the best format their browser can handle.
The <img> inside <picture> must always have alt, width, and height
attributes — the <source> elements do not take these. The <img> also
handles all the normal image semantics; the <picture> wrapper is purely
about source selection.
Compression: the easy 50%
Even choosing the right format, most images shipped on the web are undercompressed. An image exported from Figma or Photoshop at default settings carries far more data than a viewer can distinguish from an optimised version.
Tools and their typical size reductions:
- Squoosh (browser-based, interactive) — visual quality slider, immediate before/after comparison; great for learning what quality levels look like.
- ImageOptim (macOS app) — drag-and-drop lossless optimisation; safe for any image without quality loss.
- Sharp (Node.js library) — programmatic batch optimisation in a build pipeline; the standard choice for server-side image processing in web projects.
- imagemin (webpack/Rollup plugin) — runs optimisers automatically at build time; just configure it once.
Typical result: 30–80% file size reduction with no perceptible quality change. A 1.5 MB JPEG hero becomes 400 KB without a pixel of visible difference at 1440px viewport width.
Responsive images: the right size for each screen
Even an optimised image can be wasteful if the browser downloads a 2400 px wide
file to display it in a 400 px column on a mobile screen. srcset and sizes
let the browser pick the right resolution:
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 50vw"
alt="A mountain at sunrise"
width="1600"
height="1067"
>The browser reads sizes to know how wide the image will be rendered
(100% of the viewport on narrow screens, 50% on wide ones), then picks the
srcset candidate that best matches the display size and
pixel density (DPR). On a 2x Retina display
at 400 px CSS width, it fetches the 800 px file — crisp, but not the wasteful
1600 px file.
Lazy loading
Images below the fold do not need to start downloading the moment the HTML
is parsed. The loading="lazy" attribute tells the browser to defer the
download until the image is about to enter the viewport:
<img
src="section-photo.jpg"
loading="lazy"
alt="Team members at the company offsite"
width="800"
height="600"
>Lazy loading is handled entirely by the browser with no JavaScript. It reduces the initial page weight — bytes that would have been downloaded on page load are now deferred until they are needed. For pages with many below-the-fold images this is one of the highest-ROI optimisations available.
Never apply loading="lazy" to the LCP image. The hero image must load as
fast as possible; deferring it guarantees a poor LCP score.
fetchpriority — push the LCP image to the front
Even without loading="lazy", the browser treats all images as medium priority
by default. During the critical path, it competes for bandwidth with CSS,
scripts, and fonts. The fetchpriority="high" attribute tells the browser to
promote this specific image to the top of the download queue:
<img
src="hero.webp"
fetchpriority="high"
alt="A mountain at sunrise"
width="1200"
height="800"
>Combined with a <link rel="preload"> in <head>, this ensures the LCP image
starts downloading at the earliest possible moment and is prioritised over
competing resources:
<head>
<link rel="preload" as="image" href="hero.webp" fetchpriority="high">
</head>Preventing CLS with explicit dimensions
When the browser encounters an <img> without width and height attributes,
it cannot reserve space in the layout before the image loads. It renders the
surrounding text first, then jumps everything down when the image arrives — a
textbook CLS event.
The fix is simple: always declare the image's intrinsic dimensions:
<!-- bad: no dimensions, layout shifts when image loads -->
<img src="photo.jpg" alt="...">
<!-- good: browser reserves exact space before download completes -->
<img src="photo.jpg" alt="..." width="1200" height="800">If your layout uses CSS to make images fluid (max-width: 100%; height: auto),
the width and height attributes still help — the browser uses their ratio
to compute the aspect-ratio and reserve proportional space, even when the CSS
constrains the rendered width.
The aspect-ratio CSS property is an alternative approach for images where
the intrinsic dimensions are not known at authoring time (for example,
user-uploaded content). Set aspect-ratio: 16/9; width: 100%; height: auto
to reserve the correct slot before the image loads.
Where to go next
Images handled, the next most common performance liability on the web is fonts.
Font Loading Strategies explains the font-display descriptor, preloading,
font subsetting, and when the system font stack is the right answer.
Core Web Vitals
Core Web Vitals are Google's three user-experience metrics — LCP, INP, and CLS — that measure loading speed, interactivity, and visual stability.
Font Loading Strategies
Web fonts are render-blocking by default and cause invisible text — font-display and preloading turn a common performance liability into a controlled tradeoff.