On most sites images are more than half the bytes. They are also the least technical thing to fix: no code changes, and the saving is immediate.

1. Serve the size the page uses

A 4000px photograph displayed in a 600px column costs the visitor the whole 4000px. This is the single biggest saving and the most common mistake.

<img src="photo-800.webp"\n     srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1600.webp 1600w"\n     sizes="(max-width: 900px) 100vw, 600px"\n     width="800" height="450" alt="...">

sizes tells the browser how wide the image will BE, so it can choose before layout. Without it the browser assumes full width and downloads the largest.

2. WebP or AVIF, not JPEG

cwebp -q 80 photo.jpg -o photo.webp

WebP is typically 25-35% smaller than JPEG at the same quality and is supported everywhere that matters. AVIF is smaller again and slower to encode.

3. Lazy-load what is below the fold - and NOT what is above it

<img src="..." loading="lazy" decoding="async">
Never lazy-load the hero image. It is the one the visitor is waiting for, and lazy-loading it delays the largest paint - which is the number Google measures.

Always give width and height

Without them the browser does not know how much room to leave, so everything below jumps when the image lands. That jump is a layout shift and it is measured.

Check what a page actually downloads: open the network panel, filter to images, sort by size. The largest one is usually a surprise, and usually a photograph nobody resized.