Overview
Unoptimized images are the single most common cause of slow Largest Contentful Paint (LCP) scores. The fix has four parts: use the right format (WebP or AVIF), resize to the actual display size, defer off-screen images with loading="lazy", and declare width and height on every <img> so the browser reserves space before the file loads. The full context for LCP and Cumulative Layout Shift lives in core-web-vitals.
Prerequisites
cwebpandimg2webpfrom thewebppackage, oravifencfromlibavif-apps.- ImageMagick (
convert) for resizing. - A source folder of original images at 2x or higher resolution. Never upscale.
# Ubuntu / Debian
sudo apt install -y webp libavif-apps imagemagick
# macOS
brew install webp libavif imagemagickSteps
1. Keep source masters separate
Store originals at full resolution in assets/images/src/. Never overwrite them. Derived outputs go in assets/images/dist/. Add dist/ to .gitignore if the build pipeline regenerates them; commit masters.
2. Resize to display dimensions
Serving a 4000 px image in a 400 px column wastes 10x bandwidth. Resize to the largest CSS pixel size the image will appear at, then double it for 2x displays.
# 800 px wide for a column that maxes at 400 CSS px (2x)
convert src/hero.jpg -resize 800x assets/images/dist/hero-800.jpgFor responsive images you may need multiple sizes (400, 800, 1200 px). Use an array and loop:
for W in 400 800 1200; do
convert src/hero.jpg -resize "${W}x" dist/hero-${W}.jpg
done3. Convert to WebP
WebP is 25 to 35 percent smaller than JPEG at equivalent quality and supported in all modern browsers.
cwebp -q 82 dist/hero-800.jpg -o dist/hero-800.webpQuality 80 to 85 is the practical sweet spot: visually indistinguishable from 100 at roughly half the file size. Do not go below 75 for hero images.
4. Convert to AVIF for maximum compression
AVIF is 20 to 30 percent smaller than WebP at the same visual quality. Browser support is above 90 percent globally as of 2026. Generate AVIF alongside WebP and let the browser choose.
avifenc --min 20 --max 35 dist/hero-800.jpg dist/hero-800.avifLower --min and --max values mean higher quality. 20 to 35 is equivalent to JPEG quality 80 to 85.
5. Use <picture> with a JPEG fallback
<picture>
<source srcset="hero-800.avif" type="image/avif">
<source srcset="hero-800.webp" type="image/webp">
<img
src="hero-800.jpg"
alt="Team working at desks"
width="800"
height="533"
loading="eager"
fetchpriority="high"
>
</picture>Use loading="eager" and fetchpriority="high" only on the LCP image (the largest image visible in the viewport on page load). All other images get loading="lazy". See html for the full <picture> pattern.
6. Set explicit width and height attributes
Omitting these attributes causes layout shift (CLS) because the browser cannot reserve space before the image loads. Always match the intrinsic pixel dimensions of the file, not the CSS size.
<!-- width and height match the file's actual pixel size -->
<img src="hero-800.webp" width="800" height="533" alt="...">Control the CSS display size separately with max-width: 100%; height: auto;.
Verify it worked
# 1. Compare file sizes before and after.
ls -lh dist/hero-800.{jpg,webp,avif}
# 2. Run Lighthouse to confirm LCP and CLS scores.
npx lighthouse https://example.com --only-categories=performance --output=json \
| jq '.categories.performance.score, .audits["largest-contentful-paint"].displayValue'
# 3. Check that lazy images are not in the LCP candidate list.
# In Chrome DevTools > Performance > LCP section, "lazy" should not appear.Common errors
- AVIF encode is very slow. AVIF encoding is CPU-intensive. Use
--jobs 4(or the number of CPU cores) to parallelize:avifenc --min 20 --max 35 --jobs 4 src.jpg out.avif. loading="lazy"on the LCP image delays it. The LCP image must be eagerly loaded. Lazy-load only images below the fold.- Layout shift persists despite
widthandheight. CSS is overriding the aspect ratio. Addimg { height: auto; }to your stylesheet. - WebP not served despite being present. The server is sending the wrong
Content-Type. Verify withcurl -I https://example.com/hero.webp | grep content-type. Configure the web server to serveimage/webpfor.webpfiles. cwebpcommand not found. Install thewebppackage, notlibwebp. They differ on some distros.