Web Development · Performance
Web Images in 2026: AVIF, WebP, and the LCP Work Nobody Does Until It's a Problem
Images are the single biggest factor in Largest Contentful Paint for most sites. AVIF has widespread browser support now. Here's the optimization stack worth using and how to implement it.
Anurag Verma
7 min read
Sponsored
Check your site’s LCP on a mobile connection and there’s a reasonable chance an image is the culprit. Not JavaScript. Not fonts. An unoptimized JPEG or PNG that nobody revisited after it was uploaded two years ago.
Image optimization isn’t technically hard. The formats have been stable for years, the tooling is good, and browser support has caught up. What gets in the way is that it’s easy to deprioritize, because images work. They’re just slow, and slow doesn’t fail visibly the way a broken API call does. LCP scores sit in the yellow until someone decides to fix them.
This is the setup worth putting in place, and it’s not that much work.
Where Browser Support Actually Stands in 2026
WebP has been supported everywhere since 2020. Safari added support in version 14. There is no meaningful browser population that doesn’t support WebP. If you’re still serving JPEG by default, you’re leaving compression on the table for every user.
AVIF (AV1 Image File Format) has supported in Chrome since 2020, Firefox since 2021, and Safari since version 16 (2022). Global browser support is above 90%. The holdout was Safari, and it’s been more than three years since that changed.
AVIF produces smaller files than WebP at the same visual quality, especially for photos with complex detail. The encoding is slower (AVIF encoding can take 10-20x longer than WebP encoding), but this is a build-time concern, not a serving concern. For CDN-delivered content, you encode once and serve many times.
Format Comparison for Common Image Types
| Image type | Recommended format | Notes |
|---|---|---|
| Photos, hero images | AVIF, fallback WebP | Best compression ratios |
| Product images | AVIF, fallback WebP | Also handles transparency |
| UI icons, logos | SVG (preferred), WebP | Vector is usually better |
| Screenshots, diagrams | WebP or PNG | PNG for exact pixel accuracy |
| Animated images | WebP or AVIF | Both support animation; prefer over GIF |
| Open Graph / social | JPEG or WebP | Some crawlers still prefer JPEG |
Serving Multiple Formats with <picture>
The browser picks the first source it supports:
<picture>
<source srcset="/images/hero.avif" type="image/avif" />
<source srcset="/images/hero.webp" type="image/webp" />
<img
src="/images/hero.jpg"
alt="Team working at a conference table"
width="1200"
height="630"
loading="lazy"
decoding="async"
/>
</picture>
AVIF is listed first (modern browsers take it), WebP as the next choice, JPEG as the final fallback for very old clients. The <img> tag at the bottom is required: it’s the fallback and carries the alt, width, height, loading, and decoding attributes.
Responsive Images with srcset
Serving a 2400px image to a 375px phone is another common waste. srcset lets the browser pick the appropriate size:
<picture>
<source
type="image/avif"
srcset="
/images/hero-480.avif 480w,
/images/hero-768.avif 768w,
/images/hero-1200.avif 1200w,
/images/hero-2400.avif 2400w
"
sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1200px"
/>
<source
type="image/webp"
srcset="
/images/hero-480.webp 480w,
/images/hero-768.webp 768w,
/images/hero-1200.webp 1200w,
/images/hero-2400.webp 2400w
"
sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1200px"
/>
<img
src="/images/hero-1200.jpg"
alt="Team working at a conference table"
width="1200"
height="630"
/>
</picture>
The sizes attribute tells the browser how wide the image will be rendered at different viewport widths. Combined with srcset, the browser multiplies the display size by the device pixel ratio and fetches the closest match. A 375px device at 3x DPR fetches the 1200px variant, not the 2400px one.
Generating Multiple Formats at Build Time
With Sharp (Node.js)
Sharp is the standard Node image processing library. Fast, minimal dependencies, good output quality.
import sharp from "sharp";
import { readdir, mkdir } from "fs/promises";
import path from "path";
const INPUT_DIR = "public/images/source";
const OUTPUT_DIR = "public/images/optimized";
const WIDTHS = [480, 768, 1200, 2400];
const QUALITY = { avif: 60, webp: 80, jpg: 85 };
async function processImage(inputPath: string, baseName: string) {
const img = sharp(inputPath);
const { width: originalWidth } = await img.metadata();
for (const width of WIDTHS) {
if (originalWidth && width > originalWidth) continue;
const base = `${baseName}-${width}`;
await img.clone().resize(width).avif({ quality: QUALITY.avif }).toFile(
path.join(OUTPUT_DIR, `${base}.avif`)
);
await img.clone().resize(width).webp({ quality: QUALITY.webp }).toFile(
path.join(OUTPUT_DIR, `${base}.webp`)
);
await img.clone().resize(width).jpeg({ quality: QUALITY.jpg, mozjpeg: true }).toFile(
path.join(OUTPUT_DIR, `${base}.jpg`)
);
}
}
Run this as a build step. For content sites with many images, add incremental processing: track which source files have changed and only reprocess those.
With Squoosh CLI
Squoosh’s CLI is useful for batch encoding when you want to tune quality settings interactively first:
npx @squoosh/cli --avif '{"quality": 60}' --webp '{"quality": 80}' -d output/ images/*.jpg
Framework-Level Optimization
If you’re using Next.js, the next/image component handles format selection, responsive srcsets, and lazy loading automatically. It converts to WebP or AVIF on demand and caches the results.
import Image from "next/image";
export function HeroImage() {
return (
<Image
src="/images/hero.jpg"
alt="Team working at a conference table"
width={1200}
height={630}
priority // removes lazy loading for LCP images
quality={80}
/>
);
}
For Astro, the <Image> component does the same. For other frameworks, check whether there’s a built-in image component before hand-rolling your own pipeline.
The LCP Image Specifically
For the page’s LCP element (usually the largest above-the-fold image), loading="lazy" is the wrong choice. Lazy loading defers the fetch until the image is near the viewport, which means the LCP image fetch starts late.
For the LCP image:
- Remove
loading="lazy"or useloading="eager" - Add
fetchpriority="high"to tell the browser to prioritize this fetch - Add a
<link rel="preload">in the<head>so the browser discovers it early
<head>
<link
rel="preload"
as="image"
href="/images/hero-1200.avif"
imagesrcset="/images/hero-480.avif 480w, /images/hero-1200.avif 1200w"
imagesizes="(max-width: 480px) 480px, 1200px"
type="image/avif"
/>
</head>
<body>
<picture>
<source
type="image/avif"
srcset="/images/hero-480.avif 480w, /images/hero-1200.avif 1200w"
sizes="(max-width: 480px) 480px, 1200px"
/>
<img
src="/images/hero-1200.jpg"
alt="Team working"
width="1200"
height="630"
fetchpriority="high"
/>
</picture>
</body>
The preload hint ensures the browser starts fetching the AVIF version before it processes the full HTML. For pages where the LCP image is on the critical path, this alone can meaningfully improve measured LCP.
CDN Delivery
If you’re serving images via a CDN, you can often offload format conversion entirely. Cloudflare Images, Cloudinary, Imgix, and similar services accept a source image URL and return the right format based on the Accept header.
# Cloudinary example: request an image with automatic format selection
https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/sample.jpg
f_auto lets Cloudinary choose AVIF, WebP, or JPEG based on browser support. q_auto adjusts quality based on perceived visual difference. The original source file never changes; the CDN handles serving the right format to each client.
This is the lowest-friction path for sites with user-uploaded content or large existing image catalogs. You don’t need to regenerate files. Just change the delivery URL.
Checking Your Work
After making changes, check your LCP images with:
- Chrome DevTools Network tab: Look at the image requests. Are they fetching AVIF/WebP? Check the size compared to the original.
- PageSpeed Insights / Lighthouse: The “Serve images in next-gen formats” and “Properly size images” audits tell you which images still need work.
- Core Web Vitals report in Search Console: Shows LCP field data from real users, which is what Google actually uses for ranking.
The goal isn’t a perfect Lighthouse score on your dev machine. It’s a good LCP on a real device on a real connection. Run Lighthouse in “mobile” mode with CPU throttling enabled to see what actual users on mid-tier phones experience.
Sponsored
More from this category
More from Web Development
SolidJS in 2026: Fine-Grained Reactivity Without the Virtual DOM
Three.js and React Three Fiber: 3D on the Web Without the Pain
TanStack Router in 2026: Type-Safe Routing That Rewires How You Think About Navigation
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored