Images, Media, and Embedding
The Fastest Way to Tank Your Core Web Vitals
A single unoptimized image can wreck your page's performance score. Here's what happens when you write this innocently looking HTML:
<img src="hero-photo.png">
No width. No height. No alt. No lazy loading. No responsive sizing. The browser has no idea how big this image is until it downloads it, so it reserves zero space — then jolts the entire layout when the image arrives. That's a CLS (Cumulative Layout Shift) spike. If the image is 5MB, it blocks other resources. If it's the largest visible element, it determines your LCP (Largest Contentful Paint) score.
One element. Five performance and accessibility problems.
Think of embedding media like hanging a painting on a wall. Before you drill, you need to know the dimensions (width and height), the mounting type (responsive or fixed), and what to show visitors who can't see it (alt text). If you just throw a painting at the wall without measuring, it falls down, damages the drywall, and looks terrible. Same thing happens to your layout when you drop in images without metadata.
The img Element Done Right
<img
src="/images/hero.webp"
alt="Developer working on a laptop in a modern office"
width="1200"
height="800"
loading="lazy"
decoding="async"
>
Let's break down every attribute:
src— the image URL. Use modern formats: WebP is ~30% smaller than JPEG, AVIF is ~50% smaller.alt— alternative text. Describes the image for screen readers and displays when the image fails to load.widthandheight— the intrinsic dimensions. The browser uses these to calculate the aspect ratio before downloading the image, preventing layout shift.loading="lazy"— defers loading until the image is near the viewport. Saves bandwidth for below-the-fold images.decoding="async"— tells the browser to decode the image off the main thread, preventing jank.
Writing Good Alt Text
Alt text is not optional. It's required by WCAG 1.1.1 and it's one of the most important accessibility features on the web.
Guidelines:
<!-- Informative image: describe what it shows -->
<img src="chart.png" alt="Bar chart showing JavaScript as the most popular language at 65%, followed by Python at 48%">
<!-- Decorative image: empty alt (not missing alt) -->
<img src="decorative-wave.svg" alt="">
<!-- Image that's also a link: describe the destination -->
<a href="/profile">
<img src="avatar.jpg" alt="Your profile">
</a>
<!-- Complex image: brief alt + longer description -->
<figure>
<img src="architecture.png" alt="System architecture diagram showing three layers">
<figcaption>The frontend communicates with the API gateway, which routes to microservices. Each service has its own database.</figcaption>
</figure>
Rules of thumb:
- Never start with "Image of" or "Photo of" — screen readers already announce it as an image.
- Be concise but specific — "Dog" is too vague. "Golden retriever catching a frisbee in a park" is good.
- Decorative images get empty alt (
alt=""), not missing alt. Missingaltmakes the screen reader read the filename. - If the image contains text, include that text in the alt.
Responsive Images
A single image doesn't work for all screen sizes. A 2400px hero image is overkill on a phone and wastes bandwidth. HTML gives you two tools for this:
srcset and sizes
<img
src="hero-800.webp"
srcset="
hero-400.webp 400w,
hero-800.webp 800w,
hero-1200.webp 1200w,
hero-2400.webp 2400w
"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 1200px"
alt="Mountain landscape at sunset"
width="2400"
height="1600"
>
srcset— lists available image files with their widths (thewdescriptor)sizes— tells the browser how wide the image will display at each breakpoint- The browser combines
srcset+sizes+ device pixel ratio to pick the optimal file
The picture Element
Use picture when you need different image formats or different crops:
<!-- Format switching: modern browsers get AVIF, fallback to WebP, then JPEG -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Mountain landscape" width="1200" height="800">
</picture>
<!-- Art direction: different crops for different screens -->
<picture>
<source media="(max-width: 600px)" srcset="hero-mobile.webp">
<source media="(max-width: 1200px)" srcset="hero-tablet.webp">
<img src="hero-desktop.webp" alt="Team working together" width="1200" height="800">
</picture>
The img inside picture is the fallback. It's also where you put alt, width, height, loading, and decoding — those attributes go on the img, not on picture or source.
Video and Audio
<!-- Video with multiple formats and accessibility features -->
<video
width="720"
height="480"
controls
preload="metadata"
poster="video-thumbnail.jpg"
>
<source src="tutorial.webm" type="video/webm">
<source src="tutorial.mp4" type="video/mp4">
<track kind="captions" src="captions-en.vtt" srclang="en" label="English" default>
<p>Your browser doesn't support HTML video.
<a href="tutorial.mp4">Download the video</a>.
</p>
</video>
<!-- Audio -->
<audio controls preload="metadata">
<source src="podcast.ogg" type="audio/ogg">
<source src="podcast.mp3" type="audio/mpeg">
<p>Your browser doesn't support HTML audio.
<a href="podcast.mp3">Download the episode</a>.
</p>
</audio>
Key attributes:
controls— shows play/pause, volume, progress bar. Without it, users can't interact with the media.preload="metadata"— downloads just enough to get duration and dimensions. Better thanpreload="auto"(downloads the whole file).poster— thumbnail image shown before the video plays.track— captions, subtitles, or descriptions for accessibility.
Never set videos to autoplay with sound. Browsers actively block this (Chrome, Safari, Firefox all require muted autoplay). If you need autoplay (for a background video), use autoplay muted loop playsinline. But consider: autoplaying video is a significant performance cost and may cause vestibular discomfort for some users.
Embedding External Content
Iframes
<iframe
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
width="560"
height="315"
title="Video: Rick Astley - Never Gonna Give You Up"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
allowfullscreen
></iframe>
Always include title on iframes. Screen readers announce the title to describe the embedded content. Without it, users hear "iframe" with no context.
The allow attribute controls which browser features the embedded page can use (camera, microphone, autoplay, etc.). The sandbox attribute goes further, restricting scripts, forms, and navigation:
<iframe
src="https://untrusted-content.com/widget"
sandbox="allow-scripts allow-same-origin"
title="Third-party widget"
></iframe>
Production Scenario: Optimized Hero Image
Here's how to build a hero image that performs well across all devices:
<picture>
<source
srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1600.avif 1600w"
sizes="100vw"
type="image/avif"
>
<source
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w"
sizes="100vw"
type="image/webp"
>
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
sizes="100vw"
alt="Panoramic view of a code editor with a gradient background"
width="1600"
height="900"
decoding="async"
fetchpriority="high"
>
</picture>
Notice:
- No
loading="lazy"on the hero image — it's above the fold, so we want it immediately. fetchpriority="high"— tells the browser this is the most important image on the page (likely the LCP element).- Three formats: AVIF (smallest), WebP (good support), JPEG (universal fallback).
- Three sizes per format: mobile (400w), tablet (800w), desktop (1600w).
| What developers do | What they should do |
|---|---|
| Omitting width and height on images Without dimensions, the browser can't reserve space. When the image loads, everything shifts — causing CLS problems | Always include width and height to prevent layout shift |
| Using loading='lazy' on above-the-fold images Lazy loading delays the LCP image, making the page feel slower. Above-the-fold images should load immediately | Only lazy-load images below the fold. Use fetchpriority='high' for the hero image |
| Serving only JPEG/PNG without modern formats AVIF is ~50% smaller than JPEG at similar quality. WebP is ~30% smaller. Smaller files mean faster page loads | Use picture with AVIF and WebP sources, JPEG as fallback |
| Missing alt attribute on images Screen readers read the filename if alt is missing. An empty alt (alt='') correctly tells screen readers to skip decorative images | Every image needs alt: descriptive for content images, empty string for decorative images |
Challenge: Build a Responsive Media Section
Create an HTML section that includes: a responsive image with three sizes and two formats, a video with captions, and a third-party embed. Make everything accessible and performant.
Show Answer
<section>
<h2>Course Preview</h2>
<figure>
<picture>
<source
srcset="preview-400.avif 400w, preview-800.avif 800w, preview-1200.avif 1200w"
sizes="(max-width: 600px) 100vw, 800px"
type="image/avif"
>
<source
srcset="preview-400.webp 400w, preview-800.webp 800w, preview-1200.webp 1200w"
sizes="(max-width: 600px) 100vw, 800px"
type="image/webp"
>
<img
src="preview-800.jpg"
srcset="preview-400.jpg 400w, preview-800.jpg 800w, preview-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 800px"
alt="Course dashboard showing progress through HTML modules"
width="1200"
height="675"
loading="lazy"
decoding="async"
>
</picture>
<figcaption>The course dashboard tracks your progress through each module.</figcaption>
</figure>
<video width="800" height="450" controls preload="metadata" poster="video-poster.jpg">
<source src="intro.webm" type="video/webm">
<source src="intro.mp4" type="video/mp4">
<track kind="captions" src="intro-en.vtt" srclang="en" label="English" default>
<p>Your browser doesn't support video. <a href="intro.mp4">Download it here</a>.</p>
</video>
<iframe
src="https://codepen.io/team/embed/preview/example"
width="800"
height="400"
title="Live code example: CSS Grid layout"
loading="lazy"
sandbox="allow-scripts allow-same-origin"
></iframe>
</section>Key accessibility and performance points:
- Image uses
figure/figcaptionfor visible caption - Image has three sizes across two modern formats plus JPEG fallback
loading="lazy"only on below-the-fold content (not the hero)- Video has captions track and a text fallback with download link
- Iframe has a descriptive
titleandsandboxfor security - All media elements have explicit dimensions to prevent layout shift
- 1Always include width, height, and alt on every img element — dimensions prevent layout shift, alt provides accessibility
- 2Use loading='lazy' for below-the-fold images, fetchpriority='high' for the hero/LCP image
- 3Serve modern image formats (AVIF, WebP) with JPEG fallback using the picture element
- 4Videos need controls, captions (track element), and a text fallback for browsers that don't support video
- 5Always include title on iframe elements — screen readers depend on it to describe embedded content