References

Image & Video Optimization

Modern formats, responsive images, lazy loading, and video delivery

Core Patterns

  • Modern Image Formats
  • Responsive Images
  • Lazy Loading and Priority
  • Video Optimization

Modern Image Formats

Format Selection

FormatBest ForBrowser Support
WebPPhotos, illustrations95%+
AVIFPhotos (smaller than WebP)90%+
SVGIcons, logos, illustrationsUniversal
PNGScreenshots with text, transparencyUniversal
JPEGFallback for photosUniversal

Size comparison (same quality): AVIF < WebP < JPEG < PNG

Serve Modern Format with Fallback

<!-- ✅ picture element: browser picks first supported format -->
<picture>
  <source srcset="hero.avif" type="image/avif" />
  <source srcset="hero.webp" type="image/webp" />
  <img src="hero.jpg" alt="Hero image" width="1200" height="600" />
</picture>

Conversion

# Sharp (Node.js — most common)
npx sharp-cli --input photo.jpg --output photo.webp --format webp --quality 80
npx sharp-cli --input photo.jpg --output photo.avif --format avif --quality 65

# cwebp (Google command-line)
cwebp -q 80 photo.jpg -o photo.webp

# ImageMagick
convert photo.jpg -quality 80 photo.webp

Responsive Images

srcset + sizes for Responsive Images

<!-- ✅ Browser chooses correct size based on viewport + screen density -->
<img
  srcset="
    product-400.webp  400w,
    product-800.webp  800w,
    product-1200.webp 1200w
  "
  sizes="
    (max-width: 600px)  400px,
    (max-width: 1200px) 800px,
    1200px
  "
  src="product-800.webp"
  alt="Running shoes"
  width="800" height="600"
/>

sizes tells the browser how large the image will be displayed at each breakpoint (CSS px, not device px). Without sizes, browser assumes 100vw and may download too-large images.

Art Direction with picture

<!-- Different crop/composition at different viewports -->
<picture>
  <source
    media="(max-width: 600px)"
    srcset="hero-mobile.webp 600w"
  />
  <source
    media="(min-width: 601px)"
    srcset="hero-desktop.webp 1200w"
  />
  <img src="hero-desktop.webp" alt="Hero" width="1200" height="500" />
</picture>

Explicit Dimensions — Prevent CLS

<!-- ✅ REQUIRED: width + height reserve space before image loads -->
<img src="photo.webp" width="800" height="600" alt="..." />

<!-- ✅ Also valid: aspect-ratio CSS when responsive width is needed -->
<style>
  .card-img { width: 100%; aspect-ratio: 4/3; object-fit: cover; }
</style>
<img class="card-img" src="photo.webp" alt="..." />

<!-- ❌ WRONG: no dimensions → layout shift when image loads -->
<img src="photo.webp" alt="..." />

Lazy Loading and Priority

Default: Lazy Load Below-Fold Images

<!-- ✅ Browser defers loading until image is near viewport -->
<img src="below-fold.webp" loading="lazy"
     width="400" height="300" alt="..." />

<!-- ❌ Don't apply lazy to above-fold images — delays LCP -->
<img src="hero.webp" loading="lazy" alt="..." />  <!-- WRONG for hero -->

fetchpriority for LCP Image

<!-- ✅ Only ONE image per page should have fetchpriority="high" -->
<img src="hero.webp" fetchpriority="high"
     width="1200" height="600" alt="Hero" />

<!-- ✅ Deprioritize thumbnails to save bandwidth -->
<img src="thumbnail.webp" fetchpriority="low"
     loading="lazy" width="200" height="150" alt="..." />

Preload LCP Image in <head>

<!-- For background images or dynamically loaded LCP images -->
<link rel="preload" as="image" href="hero.webp"
      imagesrcset="hero-400.webp 400w, hero-800.webp 800w"
      imagesizes="100vw" />

decoding=“async”

<!-- Decode image off main thread; prevents jank during scroll -->
<img src="photo.webp" loading="lazy" decoding="async"
     width="400" height="300" alt="..." />

Video Optimization

Autoplay Ambient Video (Replace GIF)

<!-- ✅ Replaces animated GIF: no audio, preload metadata only -->
<video autoplay loop muted playsinline preload="metadata"
       width="600" height="400">
  <source src="animation.webm" type="video/webm" />
  <source src="animation.mp4" type="video/mp4" />
</video>

preload="metadata" downloads only duration/dimensions, not full video. muted is required for autoplay in most browsers.

User-Initiated Video

<!-- ✅ preload="none" for content not immediately visible -->
<video controls preload="none" poster="thumbnail.webp"
       width="1280" height="720">
  <source src="talk.webm" type="video/webm" />
  <source src="talk.mp4" type="video/mp4" />
</video>

poster shows a static image before play — critical to prevent layout shift and provide visual placeholder.

Lazy Load Video

<video data-src="heavy-video.mp4" preload="none" poster="thumb.webp">
</video>

<script>
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const video = entry.target;
      video.src = video.dataset.src;
      observer.unobserve(video);
    }
  });
});
document.querySelectorAll('video[data-src]').forEach(v => observer.observe(v));
</script>

Common Pitfalls

Applying loading="lazy" to LCP image: Delays the most important image. The LCP image should always load eagerly (default) with fetchpriority="high".

Missing sizes attribute with srcset: Without sizes, the browser assumes the image fills 100vw and may download a 1200px image for a 300px slot.

Not setting image dimensions on CMS images: CMS-served images often lack explicit dimensions. Use CSS aspect-ratio as a fallback to prevent CLS.

Converting PNG screenshots to WebP/AVIF lossy: Text/screenshots need lossless encoding. Use --lossless flag or keep as PNG.

Using GIFs: GIFs have poor compression and no hardware decoding. Replace with <video autoplay loop muted> (WebM format).