Image Optimization

Modern Image Formats: AVIF, WebP, and the JPEG XL Revival

Images remain the dominant content type on the web and are the LCP element on 76–85% of pages (2025 Web Almanac). Choosing the right format is the single easiest compression win. The format hierarchy in 2026 is clear: AVIF > WebP > JPEG/PNG as fallback.

AVIF (AV1 Image File Format) is the best format for general web delivery. Built on the AV1 video codec, it achieves roughly 50% smaller files than JPEG and 20% smaller than WebP at equivalent visual quality. AVIF excels at preserving detail in noisy images and sharp gradients where WebP tends to blur. It supports lossy and lossless compression, transparency, HDR, wide color gamuts (10/12-bit), and animation. All current versions of Chrome, Edge, Firefox, Opera, and Safari support AVIF. The trade-off: AVIF encoding is computationally expensive, and decoding is slower than WebP on low-end devices — potentially impacting LCP for hero images on budget phones.

WebP remains the universal workhorse. Developed by Google in 2010, it offers ~30% smaller files than JPEG with 100% browser support. WebP decodes faster than AVIF, making it the safer choice for LCP hero images where decode speed matters. It supports lossy, lossless, transparency, and animation in a single format. For most sites, WebP is “good enough” and the lowest-risk modern format.

JPEG XL has had a dramatic trajectory — Google dropped Chromium support in 2022, but support is returning. JXL offers lossless transcoding of existing JPEGs (~20% size reduction without re-encoding), fast decoding (often faster than both AVIF and WebP), progressive loading, CMYK support for print workflows, and 32-bit depth. It’s gaining momentum among photographers, enterprise imaging pipelines, and CDN providers. For now, treat JPEG XL as a progressive enhancement: serve it where supported, with AVIF/WebP fallbacks.

The recommended <picture> pattern serves the most efficient format each browser supports:

<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Description"
       width="800" height="600"
       loading="lazy"
       decoding="async">
</picture>

Resources:

Compression Tools: Squoosh and SVGOMG

Squoosh (squoosh.app), developed by the Chrome team, is a browser-based image compression tool that provides real-time visual comparison between the original and compressed output. It supports AVIF, WebP, JPEG, PNG, and several other formats, with fine-grained control over quality settings, resize dimensions, and advanced codec options. Squoosh runs entirely in the browser using WebAssembly (no server upload needed), making it ideal for quick one-off optimizations and for understanding the quality/size trade-offs of different formats and settings. For batch processing, Squoosh also ships a CLI (@aspect-build/squoosh) that can be integrated into build pipelines.

For programmatic workflows, Sharp (Node.js, powered by libvips) is the standard library for server-side image processing — resizing, format conversion, and compression at high speed with low memory usage. mozJPEG provides the best JPEG compression (used by many image CDNs), while libavif and cwebp handle AVIF and WebP encoding respectively.

SVGOMG (jakearchibald.github.io/svgomg/) is Jake Archibald’s browser-based GUI for SVGO (SVG Optimizer), which removes unnecessary metadata, comments, editor artifacts, and redundant elements from SVG files. SVGs from design tools like Figma, Sketch, and Illustrator routinely contain 30–60% unnecessary data. SVGOMG provides a toggle interface for each optimization pass, letting you see the impact of each change in real time. For build-time SVG optimization, integrate SVGO directly via vite-plugin-svgo or the svgo CLI.

Resources:

Responsive Images: srcset, sizes, and the <picture> Element

Serving a single image file to all devices means either mobile users download desktop-sized images (wasting bandwidth and hurting LCP) or desktop users see blurry images. Responsive images solve this by giving the browser a menu of image options and the information to choose the right one.

Resolution switching with srcset and sizes handles the most common case — the same image at different sizes for different viewport widths:

<img
  srcset="photo-400.jpg 400w,
          photo-800.jpg 800w,
          photo-1200.jpg 1200w,
          photo-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw,
         (max-width: 1200px) 50vw,
         33vw"
  src="photo-800.jpg"
  alt="A scenic landscape"
  width="1600" height="900"
>

The srcset attribute lists available files with their intrinsic width (w descriptor). The sizes attribute tells the browser the display width of the image at each breakpoint. The browser combines this with the device pixel ratio (DPR) and viewport width to select the optimal file. A phone with a 375px viewport at 2× DPR viewing a full-width image needs a 750px-wide file — the browser picks photo-800.jpg.

Pixel density switching with 1x/2x descriptors is simpler and appropriate when the image is always displayed at a fixed CSS size (e.g., a logo or avatar):

<img
  srcset="logo.png 1x, logo@2x.png 2x, logo@3x.png 3x"
  src="logo.png"
  alt="Company logo"
  width="200" height="50"
>

This serves the standard image to 1× screens and the high-resolution version to Retina/HiDPI displays, avoiding unnecessary downloads on standard screens.

Compressing for high-density screens: Jake Archibald’s essential article “Halve the Size of Images by Optimising for High Density Displays” reveals that 80%+ of users are on screens with DPR 1.5 or above. The key insight: images displayed at 2× density can tolerate much lower compression quality because the human eye can’t discern compression artifacts at that pixel density. A 2× image at quality 44 (21KB) looks as good as a 1× image at quality 80 (15KB) when displayed at its intended density — but most sites encode 2× images at the same high quality (45KB), shipping images 100% heavier than needed. The “lazy” approach: encode at 2× the maximum CSS display size at lower quality, and serve via <picture> with AVIF/WebP/JPEG fallbacks. The “full” approach: use <source media="(-webkit-min-device-pixel-ratio: 1.5)"> to serve different quality tiers to high-density vs standard screens.

The <picture> element provides art direction and format switching. Use it when you need different crops for different screen sizes (art direction), or to serve modern formats with fallbacks:

<picture>
  <!-- Art direction: crop tightly on mobile -->
  <source media="(max-width: 600px)"
          srcset="hero-mobile.avif 600w"
          type="image/avif">
  <source media="(max-width: 600px)"
          srcset="hero-mobile.webp 600w"
          type="image/webp">
  <!-- Desktop: wider crop -->
  <source srcset="hero-desktop.avif 1200w, hero-desktop.avif 1800w"
          sizes="100vw"
          type="image/avif">
  <source srcset="hero-desktop.webp 1200w, hero-desktop.webp 1800w"
          sizes="100vw"
          type="image/webp">
  <img src="hero-desktop.jpg" alt="Hero image"
       width="1800" height="600"
       fetchpriority="high">
</picture>

Always include width and height attributes on all <img> elements. Modern browsers use these to calculate the aspect ratio before the image loads, reserving the correct space and preventing Cumulative Layout Shift (CLS). Without explicit dimensions, the browser can’t reserve space, and the image will cause a layout shift when it loads.

Resources:

LCP Image Optimization: fetchpriority and Preloading

The LCP element is an image on 76–85% of web pages. Optimizing how quickly that image loads is the single most impactful LCP improvement. Barry Pollard (Google Chrome Web Performance Developer Advocate) emphasizes that the problem is usually not image file size — it’s loading priority and the critical rendering path.

fetchpriority="high" on the LCP image tells the browser to prioritize this image over other resources. Without it, images compete equally with other subresources; with it, the browser fetches the LCP image as early as possible. This is one of the highest-impact single-attribute changes you can make, yet only a small percentage of sites use it — the 2025 Web Almanac notes that 0.3% of sites even set fetchpriority="low" on their LCP image, almost certainly by accident.

Preload the LCP image in the document <head> for the fastest possible discovery. The browser’s preload scanner discovers resources in HTML order — if your LCP image is deep in the DOM or loaded via CSS background-image, the browser won’t find it until late. A preload link solves this:

<link rel="preload" as="image" href="hero.avif"
      type="image/avif" fetchpriority="high">

For responsive images, use imagesrcset and imagesizes on the preload:

<link rel="preload" as="image"
      imagesrcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w"
      imagesizes="100vw"
      type="image/avif">

Never lazy-load the LCP image. The 2025 Web Almanac reports that ~17% of pages lazy-load their LCP image — this actively harms LCP by delaying the most important image on the page. Lazy loading is for below-the-fold images only.

Resources:

Lazy Loading: Native, IntersectionObserver, and Viewport Proximity

For below-the-fold images, lazy loading defers the download until the image is needed, saving bandwidth and reducing contention with critical resources. There are two approaches:

Native lazy loading (loading="lazy") is the simplest — add the attribute and the browser handles everything. All modern browsers support it. The browser uses internal heuristics to start loading the image before it enters the viewport (typically 1250–2500px ahead on fast connections, closer on slow ones). For most cases, this is all you need.

IntersectionObserver provides finer control. You can specify exactly when to trigger loading via the rootMargin option — for example, start loading when the image is 300px below the viewport:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.srcset = img.dataset.srcset;
      observer.unobserve(img);
    }
  });
}, { rootMargin: '300px 0px' }); // Start loading 300px before viewport

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

This “viewport proximity” approach balances bandwidth savings with UX — the image starts loading before the user scrolls to it, so there’s no visible blank space. For the LCP image or any above-the-fold content, always use loading="eager" (the default) with fetchpriority="high".

decoding="async" on <img> elements prompts the browser to decode the image off the main thread, reducing CPU time that would otherwise block rendering. Combined with content-visibility: auto (which skips layout for off-screen sections entirely), these attributes form a comprehensive strategy for image-heavy pages.

Resources:

Progressive Loading and Perceived Performance

Even with optimized formats and lazy loading, large images take time to download. Progressive loading techniques improve perceived performance by showing something meaningful immediately while the full image arrives:

Low-Quality Image Placeholders (LQIP) show a tiny, heavily blurred version of the image (~1–2KB inline as a base64 data URI or a small file) while the full image loads. When the full image arrives, it fades in over the placeholder. This technique is used by Medium, Facebook, and many image CDNs. The LQIP provides immediate visual context (layout, color, rough composition) without the cost of the full download.

BlurHash encodes the image’s color palette and structure into a very compact string (20–30 characters) that can be decoded client-side into a blurred gradient placeholder. Unlike LQIP (which requires a small actual image), BlurHash is a pure data representation that’s stored alongside image metadata — ideal for API-driven galleries where you don’t want an extra image request per item.

CSS background-color placeholders are the simplest approach: set a background-color on the image container that matches the image’s dominant color. This prevents the jarring white-to-image flash and costs zero bytes.

Progressive JPEGs render in multiple passes — a blurry full-size preview appears first, then progressively sharpens. Jake Archibald’s definitive 2025 article “The Present and Potential Future of Progressive Image Rendering” benchmarks progressive rendering across all formats: progressive JPEG starts rendering at ~1.2KB and completes its first pass at ~33KB (21% of file size) with a ~40% decode time penalty (negligible on modern hardware). AVIF has experimental layered progressive support (via avifenc --layered), showing an initial render at ~5.8KB (4% of data) in Chromium, but the feature is still experimental. WebP renders top-to-bottom in Firefox/Chrome but Safari shows nothing until the full file arrives. JPEG XL supports progressive rendering in the format spec but Safari (the only browser with JXL support) doesn’t actually implement progressive display. Archibald’s key insight: with modern formats producing small files (56KB for an HD AVIF), and network data arriving in bursts rather than gradually, the practical benefit of progressive rendering is more limited than commonly assumed — but it remains valuable for larger hero images on slow connections.

Resources:

Image CDNs: Automatic Optimization at the Edge

Image CDNs automatically optimize, resize, convert, and cache images on-the-fly via URL parameters. Instead of generating multiple image variants at build time, you upload the original and the CDN serves the optimal format, size, and quality for each user’s device and browser. This eliminates the complexity of maintaining responsive image pipelines and ensures images are always served in the best format supported by the requesting browser.

Cloudinary is the most feature-rich option, offering on-the-fly transformations (resize, crop, overlay, effects), automatic format selection (f_auto serves AVIF/WebP/JPEG based on browser support), automatic quality (q_auto), responsive breakpoint generation, and AI-powered smart cropping. Imgix provides similar capabilities with a URL-based API and strong developer tooling. Cloudflare Images offers simpler, CDN-integrated image optimization with per-variant storage. Vercel Image Optimization (via next/image) provides automatic optimization integrated into Next.js with on-demand resizing, format conversion, and caching at the edge.

For self-hosted solutions, Sharp (Node.js) or libvips can power an image processing service behind your own CDN. The key is to never serve the original upload directly to users — always serve a processed, appropriately sized, modern-format version.

Resources:

Video Optimization: Replace GIFs, Optimize Delivery

Animated GIFs are one of the worst offenders for page weight. A 5-second GIF can easily be 5–10MB. Replace them with auto-playing, looping, muted <video> elements using modern codecs:

<video autoplay loop muted playsinline>
  <source src="animation.av1.mp4" type="video/mp4; codecs=av01.0.05M.08">
  <source src="animation.h264.mp4" type="video/mp4">
</video>

AV1 video provides the best compression (often 50%+ smaller than H.264 at equivalent quality). For video poster images, match the video’s aspect ratio and use object-fit: cover to prevent reflows. Preload the poster with fetchpriority="high" if the video is the LCP element. Process videos with HandBrake (free, cross-platform) for multipass encoding, move the moov atom to the file head for faster start times, and enable byte-range serving on your server for efficient seeking.

Resources: