Web Performance

WebP and AVIF Image Optimization That Actually Improves LCP

Hero images dominate LCP on most marketing pages I audit. Switching to AVIF/WebP helps only if sizing, priority, and encoding are right—here is the workflow that moved real numbers.

What you will learn

  • Fix the LCP image first: format, size, priority, discovery
  • Serve AVIF → WebP → JPEG/PNG via picture
  • Never lazy-load the LCP asset
  • Match preload to the actual painted resource
  • Responsive widths matter as much as codec choice
  • Validate with mobile field or throttled lab data

LCP is usually an image problem

On content and landing pages, the largest contentful paint is often a hero image or a big poster frame. Compressing that asset, serving a modern format, and helping the browser discover it early beats most framework tricks.

I once “optimized” a site by lazy-loading the hero. LCP got worse because the browser deprioritized the most important paint. Rule: LCP image gets eager loading and high fetch priority; everything below the fold can be lazy.

Confirm the LCP element in field data or a throttled lab run before you spend a week converting a thousand blog thumbnails. Fix the winner first.

Lighthouse lab scores on desktop hid our mobile LCP pain. Always throttle CPU and network or use field data. Optimizing for an unthrottled MacBook creates false confidence.

Fonts can also be LCP, but on the marketing pages I audit, images win. Still check: if LCP is a text block, your image work will not move the metric no matter how clever the codec.

Encode once, serve many

Generate AVIF and WebP from a high-quality source (not from an already crushed JPEG). Keep a JPEG/PNG fallback for ancient clients. I aim for visually lossless enough at the sizes we actually display—not a 2400px asset in a 600px slot.

Typical starting points: AVIF quality around 45–60, WebP 70–80, then eyeball on a real phone. Automate with Squoosh CLI, sharp, or your CDN’s transformer so humans do not hand-export every blog thumbnail.

Screenshots with UI chrome often need higher quality than photos. Watch for banding in gradients—AVIF can look great at small sizes and weird on flat brand colors if you push too hard.

Preserve ICC profiles when color accuracy matters for brand assets. Blind conversion pipelines sometimes shift colors enough that design rejects the “optimized” hero.

Keep the source asset in the repo or DAM at full quality. Re-encoding from a previously compressed WebP stacks generation loss and makes future recompression ugly.

# Example with sharp in Node
import sharp from "sharp";

await sharp("hero.png")
  .resize({ width: 1600, withoutEnlargement: true })
  .webp({ quality: 75 })
  .toFile("hero-1600.webp");

await sharp("hero.png")
  .resize({ width: 1600, withoutEnlargement: true })
  .avif({ quality: 50 })
  .toFile("hero-1600.avif");

Markup that negotiates formats correctly

Use `<picture>` with AVIF first, then WebP, then fallback. Include width/height or aspect-ratio CSS to stop layout shift. Match `sizes` to your real CSS breakpoints—wrong sizes downloads a huge file on mobile.

Art direction (different crop on mobile) belongs in separate `<source media>` entries. Do not stretch a desktop wide crop into a phone hero and call it responsive.

Decode cost matters on low-end Androids. A slightly larger WebP that decodes faster can beat a tiny AVIF for LCP in the wild. Spot-check on a cheap device, not only on your flagship.

<link
  rel="preload"
  as="image"
  href="/images/hero-1600.avif"
  type="image/avif"
  imagesrcset="/images/hero-800.avif 800w, /images/hero-1600.avif 1600w"
  imagesizes="100vw"
  fetchpriority="high"
/>

<picture>
  <source
    type="image/avif"
    srcset="/images/hero-800.avif 800w, /images/hero-1600.avif 1600w"
    sizes="100vw"
  />
  <source
    type="image/webp"
    srcset="/images/hero-800.webp 800w, /images/hero-1600.webp 1600w"
    sizes="100vw"
  />
  <img
    src="/images/hero-1600.jpg"
    width="1600"
    height="900"
    alt="Product dashboard on a laptop"
    fetchpriority="high"
    decoding="async"
  />
</picture>

Mistakes that made LCP worse

Preloading a WebP while the page actually paints an AVIF from `<picture>` wastes bandwidth and confuses prioritization—preload the same URL the LCP element will use. Shipping only a 3000px AVIF “because AVIF is small” still decodes slowly on mid-range phones.

Another miss: CSS background-image heroes. They are harder to preload and often invisible to LCP candidates the way you expect. Prefer an `<img>` in the document for the true LCP media.

Third miss: putting the hero behind a client-only framework render with no SSR/HTML placeholder. The browser cannot preload what does not exist in the first HTML byte.

Priority hints are hints. If the HTML discovers the hero late because it sits under a banner injected by JS, fix discovery order before tweaking quality dials.

  • Never lazy-load the LCP image
  • Set explicit dimensions to protect CLS
  • Serve responsive widths, not one giant file
  • Preload must match the resource that actually paints
  • Compress art direction crops separately when the mobile crop differs

CDN and caching practicalities

If your CDN can convert on the fly (`format=auto`), great—still set sane max widths. Cache-Control should be long-lived with fingerprinted filenames. I measure before/after with WebPageTest or CrUX, not only Lighthouse on a cable connection.

On one brochure site, AVIF cut hero transfer from ~450KB JPEG to ~90KB AVIF. LCP dropped about 0.8s on mid-tier mobile once we also fixed `sizes` and removed lazy-loading. Format alone was not the whole win.

Watch Content-Type headers from origin. Serving AVIF as octet-stream breaks negotiation in some setups. Be explicit.

SVGs for simple illustrations beat photos encoded as AVIF. Do not rasterize logos. I still find PNG logos in heroes that should have been SVG five years ago.

Vary on Accept only if you truly negotiate at the CDN and understand cache key explosion. Many teams are safer generating distinct URLs per format and width.

A short production checklist

Identify the LCP element in field data. Encode AVIF+WebP+fallback at correct widths. Wire `<picture>` + dimensions + fetchpriority. Preload carefully. Re-measure on a throttled mobile profile. Then optimize the rest of the page.

Do not boil the ocean converting every archive thumbnail before fixing the hero. LCP cares about one dominant element first.

Automate encoding in CI so new marketing pages cannot ship a 2MB PNG “temporary” asset that becomes permanent.

Make image budgets part of PR review. A 1.5MB hero should fail CI the same way a failing test does. Cultural enforcement beats wiki pages nobody reads.

Record before/after LCP in the PR description with a WebPageTest link. Performance work without numbers regresses the first time someone swaps in a new brand photo.

Below-the-fold images after the hero is fixed

Once LCP is healthy, lazy-load the rest with loading="lazy" and sensible widths. Native lazy loading is enough for most article pages; custom IntersectionObserver loaders are rarely worth it now.

For carousels, do not load every slide’s full AVIF up front. Load the active slide eagerly and hydrate neighbors on interaction or near-viewport approach.

<img
  src="/images/article-1.webp"
  width="800"
  height="450"
  alt=""
  loading="lazy"
  decoding="async"
/>

Key takeaways

  • Fix the LCP image first: format, size, priority, discovery
  • Serve AVIF → WebP → JPEG/PNG via picture
  • Never lazy-load the LCP asset
  • Match preload to the actual painted resource
  • Responsive widths matter as much as codec choice
  • Validate with mobile field or throttled lab data

About the author

Ram — Founder & Editor, BudhiWorks. I build and ship production web apps — Node, React/Next.js, Postgres, and the boring infrastructure that keeps them online. BudhiWorks is where I publish the guides I wish I had when something broke at 2 a.m.

More about Ram · Contact

← Debounce vs Throttle in JavaScript (With C… Next: API Rate Limiting with Token Buckets and R… →