Images, Video and the Elements That Own Their Layout
Replaced elements bring their own dimensions, their own bytes and their own decode memory — and nearly every problem they cause is fixed with an attribute rather than with code.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What do the media elements actually do, and which attributes change the browser's work rather than the appearance?
A person wants to see the photo, watch the clip or read the chart — and if they cannot see it, to be told what it shows.
An <img src> with width: 100% in CSS is enough. Video is a <video autoplay>. Alt text is a field to fill in before launch.
Without width and height attributes or a CSS aspect-ratio, the box is zero until the bytes have arrived and decoded. Then it takes its real size and shoves everything below it down the page, usually while someone is reading (Visual Stability).
- Without
widthandheightattributes or a CSSaspect-ratio, the box is zero until the bytes have arrived and decoded. Then it takes its real size and shoves everything below it down the page, usually while someone is reading (Visual Stability). loading="lazy"on a hero image delays the largest paint on the page: the browser will not even request it until layout has decided it is near the viewport, which is a round trip added to the one thing the metric is measuring.- A 4000×3000 photo is roughly 48 MB once decoded, no matter that the file was 800 KB. CSS scaling it to 400px wide changes the paint, not the decode. On a mid-range phone, a gallery of these is a discarded tab (Memory Leaks).
- A missing
altattribute is not the same asalt="". Missing means the screen reader falls back to reading the file name; empty means the image is decorative and correctly skipped. - Autoplay with sound is blocked by every major browser.
play()returns a promise that rejects, and code that ignores it produces an unhandled rejection and a video that never starts. - Without
sizes, the browser evaluatessrcsetassuming the image occupies the full viewport width, so a thumbnail in a grid downloads the candidate intended for a full-bleed hero.
What is actually happening
In the browser, not in the framework.
- Media elements are replaced elements: their content comes from outside the document, and their intrinsic size comes from the resource.
widthandheightattributes give the layout engine an aspect ratio it can use before the resource exists, which is the whole mechanism behind reserving space. - The preload scanner discovers
imgsources in the byte stream and requests them ahead of the main parser, which is why an image referenced in HTML is usually requested earlier than one set from JavaScript or from a CSS background (The Preload Scanner). srcsetandsizesdescribe candidates; the browser chooses. It weighs device pixel ratio, the resolved layout width fromsizes, and its own policy, which may include cache state and network conditions. You are supplying options, not making the decision (Responsive Images).<picture>with<source type>evaluates sources in document order and takes the first the browser can handle, which is how format negotiation and art direction are expressed in markup rather than in a build step.- Decoding turns compressed bytes into a bitmap in memory sized by pixel dimensions, not file size. Decoding usually happens off the main thread; the resulting memory is charged to the renderer regardless.
- Video is a separate media pipeline: its own buffering, its own decode — frequently hardware-accelerated — and a composited output surface, which is why a playing video keeps producing frames while the main thread is busy (Compositing Layers, and Accelerators: The Specialization Spectrum in Computer Architecture).
What this makes the browser do
And which of it is avoidable.
- Layout: replaced-element sizing. With a known ratio it happens once, before the bytes arrive. Without one it happens again on arrival, invalidating everything below the image in the flow (Style Invalidation).
- Decode: proportional to pixel count. This is the media cost that is invisible in the Network panel, because transfer size and decode cost are unrelated.
- Paint and composite: large images become GPU textures. Many large images become many textures, and texture memory is a scarcer resource than page memory on phones (Layer Explosion).
- Video: continuous decode and composite every frame for the whole duration. A muted looping background video is a permanent, ongoing cost that a still image is not (The Frame Budget).
- The avoidable work: downloading candidates larger than the layout box, decoding images that will never be scrolled into view, and re-laying-out because the ratio was not declared.
What the attributes cost
Media is where the render-cost question has the least intuitive answers, because the expensive stages are not the ones the change appears to touch. Setting an aspect ratio in markup costs nothing at all and *removes* a layout pass later. Swapping a source costs a decode that never appears in the Network panel.
Read the maybe rows carefully — they are the honest ones. Whether swapping an image source triggers layout depends entirely on whether the new resource has the same intrinsic ratio as the old, which is a property of your asset pipeline rather than of the browser.
| Change | style | layout | paint | composite | Why |
|---|---|---|---|---|---|
| Adding `width` and `height` attributes | no | no | no | no | Pure gain: the ratio is known before the bytes arrive, so the first layout is already correct and the second one never happens. |
| An image arriving with no declared ratio | no | yes | yes | maybe | The box grows from zero to its intrinsic size, invalidating layout for everything after it in the flow. This is the mechanism of media-driven layout shift. |
| Swapping `src` on a visible image | no | maybe | yes | maybe | Layout only if the intrinsic ratio changed or no ratio was declared. Paint always; a new decode and a new texture upload. |
| `loading="lazy"` on an offscreen image | no | no | no | no | Defers the request, the decode and the texture entirely — as long as space was reserved, so its later arrival shifts nothing. |
| A playing `<video>` | no | no | no | yes | Frames go straight to a composited surface, often from hardware decode. Cheap per frame on the main thread and continuous for the whole duration. |
| Inlining an SVG instead of using `<img>` | yes | yes | yes | no | The graphic becomes DOM: every node is styled and laid out. Sixty inline icons is sixty subtrees through every style pass (Style Calculation). |
| `object-fit` change on a loaded image | yes | no | yes | maybe | The box is unchanged; only how the resource is fitted into it changes, so the geometry pass is skipped. |
caveat Every maybe here resolves against the rest of the page. An image inside a container with a fixed size and contain: layout cannot propagate a layout invalidation outwards at all, which changes several of these rows (CSS Containment).
Choosing the element
The elements are not interchangeable, and the difference that matters most is whether the graphic is in the DOM. Inline SVG is markup: styleable, scriptable, accessible, and charged to the style and layout passes. Everything else is an opaque resource with an intrinsic size.
The costs column is where the real decision lives. A CSS background image is invisible to the preload scanner and cannot carry alt text; a canvas has no semantics at all and needs an alternative you write yourself. Both are correct choices in the right place and quiet accessibility failures in the wrong one.
What is this image doing on the page, and who needs to be able to perceive it?
when A photograph or bitmap that carries content and needs to work at multiple resolutions.
cost Verbose markup that wants generating; you must declare dimensions and write alt text that is actually about its function.
when Format negotiation, or genuine art direction where the crop changes by breakpoint.
cost Multiple encoded variants to build, store and invalidate; more markup per image (CDN Delivery).
when An icon or diagram that must inherit colour, animate, or expose parts to assistive technology.
cost Real DOM nodes in every style and layout pass, and duplicated bytes if the same icon appears many times (Div Soup: How It Happens and What It Costs).
when A logo or icon that never needs to change colour or be inspected.
cost Opaque: cannot inherit currentColor, cannot be styled or scripted; caches and reuses well in exchange.
when Purely decorative texture with no informational content.
cost Invisible to the preload scanner, discovered a round trip late, no alt text, and no intrinsic sizing — never use it for content (Render-Blocking Resources).
when Content generated at runtime: a data visualisation, a game, an image editor.
cost Zero semantics. Every bit of accessibility is yours to build, and the pixels are opaque to find-in-page, zoom and translation.
when Actual moving footage the user came to watch.
cost Continuous decode and composite, captions and transcripts to produce, an autoplay policy to handle, and a large first-byte cost (The Frame Budget).
A media player everyone can use
Video is where the accessibility bill is largest and most often unpaid, because the required artefacts — captions, transcripts, audio descriptions — are content production rather than engineering. That does not make them optional; it makes them a scheduling problem to raise early rather than a ticket to write late.
The engineering half is smaller than it looks. controls alone gives you a keyboard-operable, screen-reader-operable player on every platform. The moment a design replaces it, every key and every announcement below becomes something you own and must test on each platform separately.
semantics <video controls poster preload="metadata"> with a <track kind="captions" srclang="en" label="English">. The native control set carries platform-appropriate roles and labels for every button.
| Space / K | Play and pause when the player has focus — the platform convention, supplied by the browser. |
| Arrow Left / Right | Seek backwards and forwards by a platform-defined step. |
| Arrow Up / Down | Volume, when the volume control has focus. |
| F | Fullscreen in most engines; a custom player must reimplement this and the exit path, including Escape. |
| C | Toggles captions where the engine supports it; otherwise the captions button is a normal focusable control. |
- — The player is a single tab stop that then exposes its controls; a custom player is usually N tab stops and needs an explicit, tested order.
- — Entering and leaving fullscreen must not lose focus — this is the most common defect in hand-built players.
- — Controls that auto-hide must never hide while focus is inside them.
- — Play and pause state changes, and the current time as it is seeked.
- — The presence of captions, and which track is active.
- — The accessible name of the video, which comes from an
aria-labelor an associated caption — not from the file name.
usually broken by Replacing controls with a design-system player and shipping it without captions, without keyboard seeking, and without a focus-safe fullscreen transition. The visual result is indistinguishable in a screenshot and the player is unusable without a mouse and unusable without hearing.
How to build it
Most important first.
- Always set
widthandheightattributes with the image's *intrinsic* dimensions, then size it with CSS. The attributes supply the ratio; the CSS supplies the display size. This is the fix for the majority of layout shift on content sites. - Write
altfor the image's function in this context, not for its contents. The same photograph is "Team photo" in an article and "" next to a caption that already describes it. - Use
srcsetandsizesfor resolution switching and<picture>for art direction or format negotiation. Serving one large image to every device is the single biggest byte win available on most pages (Images and Fonts). - Lazy-load below the fold only, and mark the largest above-the-fold image with
fetchpriority="high". Never lazy-load an image that is visible on load. - For video: a
poster,preload="metadata", realcontrols, a<track kind="captions">, and neverautoplaywith sound. If a background video autoplays, it must be muted,playsinline, and gated onprefers-reduced-motion(Contrast, Colour and Motion). - Choose deliberately between inline SVG — in the DOM, styleable, part of the accessibility tree, and a cost in nodes — and
<img src="….svg">, which is an opaque image with none of those properties (Div Soup: How It Happens and What It Costs).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Alt text describes function, not pixels. If the image is a link or a button, the alt text is the destination or the action — not a description of the icon.
alt=""is a positive statement that the image is decorative and should be skipped. It is not the same as omitting the attribute, which makes assistive technology guess, usually by reading the URL.- Complex images — charts, diagrams, maps — need a full description in text near the image, or referenced from it. No alt attribute is long enough to convey a chart, and a chart is exactly the case where the information exists nowhere else on the page.
- Video needs captions for dialogue and important sound, a transcript for people who would rather read, and audio description where meaningful information is only visual. Captions are not subtitles: subtitles translate dialogue, captions convey sound.
- A custom video player must be fully keyboard operable — play, pause, seek, volume, fullscreen, captions — and this is where most of them fail. The native
controlsattribute is operable by keyboard and by every assistive technology, for free. - Inline SVG needs
role="img"with a<title>when it conveys meaning, oraria-hidden="true"when it is decorative next to a text label.<canvas>has no semantics whatsoever and requires an accessible alternative you supply yourself. - Autoplaying motion and animated GIFs must respect
prefers-reduced-motion; for some users this is a vestibular trigger, not a preference (Contrast, Colour and Motion).
What can go wrong
- Lazy-loading the LCP image because a lint rule said to lazy-load images. The rule was right about the other forty.
- Enormous intrinsic images downscaled with CSS: the page looks correct, the byte count looks acceptable after compression, and the tab is evicted on a phone.
- Alt text that duplicates an adjacent caption, so the content is announced twice in a row.
alt="chart"on a chart. The information the image carries is exactly what the alternative text is for; a complex image needs a real description nearby, not a label.- A
<track>file that 404s. The captions button appears, does nothing, and reports no error anywhere a developer will see it. object-fit: covercropping the subject out of the frame at narrow widths, which passes every review conducted on a laptop.- A CDN transform URL cached with the wrong variant, so one device class receives a version sized for another everywhere it is cached (CDN Delivery).
- An unhandled rejection from
play()when the autoplay policy blocks it, which surfaces in error tracking as noise and hides real failures (Frontend Error Tracking).
- Decode racing paint: the image can be laid out, painted as empty, and then repainted when the decode completes.
decoding="sync"and thedecode()promise exist to control which side of a frame that lands on. - A viewport resize re-evaluates
srcsetand may select a different candidate mid-load, leaving two requests in flight for the same image. - A
play()promise racing a user navigation: the video begins buffering, the route changes, and the promise settles against an element that is no longer in the document (Cancelling a Request Nobody Is Waiting For).
- Images are a cross-origin communication channel that the same-origin policy deliberately does not block. Loading one tells the third-party origin that this user visited this page, with a referrer, whether or not any script is involved (Third-Party Scripts and the Supply Chain).
- Drawing a cross-origin image onto a canvas taints it, and
getImageDatathen throws. That is a privacy control, not a bug —crossoriginplus a permissive CORS response is the opt-in (CORS). - User-uploaded SVG served inline from your origin executes script in your origin. Serve user images from a separate origin or as attachments, and sanitise SVG specifically (File Upload Security in Security Engineering).
- Uploaded photographs carry EXIF metadata, which routinely includes GPS coordinates. Strip it server-side; the browser will happily upload and display it (File Uploads Through the Backend in Backend Engineering).
- A server that fetches a user-supplied image URL to resize or proxy it is a server-side request forgery primitive (Server-Side Request Forgery (SSRF) in Security Engineering).
img-srcin a Content-Security-Policy constrains where images may load from, andreferrerpolicy constrains what those requests leak about the current URL (Content Security Policy).
- "Sizing images with CSS is enough." CSS applies after the box has been laid out at least once. The attributes are what let the first layout be correct.
- "Lazy loading is always a win." It is a win for images the user may never see and a regression for the one the page is judged on.
- "File size is the image cost." Transfer size is one cost; decode memory, scaled by pixel count, is the one that gets tabs killed on phones.
- "
altis for search engines." It is the content of the image for anyone who cannot see it, and the fallback when the image fails to load. - "Muted autoplay is free." It is a continuous decode and composite for as long as it plays, and it is motion that some users have asked their operating system to stop.
- "Captions and subtitles are the same." Subtitles assume you can hear and translate dialogue; captions convey dialogue *and* meaningful sound for someone who cannot.
Measuring it, and what changes in the field
- The Network panel shows transfer size and, on the element,
currentSrcreveals whichsrcsetcandidate the browser actually chose — usually the fastest way to discover thatsizesis wrong. - The Performance panel identifies the LCP element and separates resource load delay from load time, which distinguishes "the image is large" from "the image was discovered late" (Loading: Why Content Arrives Late).
- The layout-shift overlay in the Rendering pane highlights the shifting region, which almost always turns out to be a media element with no declared ratio.
- Decoded image memory appears in the renderer's memory footprint and nowhere in transfer size. If a page is heavy on a phone and light on the wire, this is where to look (Debugging Memory).
- Field data is the only place autoplay blocking, codec fallbacks and slow-network candidate selection are visible at all (Real User Monitoring).
- On a device-pixel-ratio 3 display, a 400px-wide box wants a 1200px-wide image — nine times the pixels, and nine times the decode memory, of the 1x version (The Viewport and Device Pixels).
- On a slow network, image priority and discovery order dominate: an image referenced from CSS is found a round trip later than one in the HTML.
- On a low-memory device, decoded image memory is a leading cause of tab discard, and the page comes back rebuilt from scratch (The Multi-Process Browser).
- With a data-saver preference set, the browser may bias candidate selection and some proxies transcode images without asking, so what you served is not necessarily what was displayed.
- Offline or on a broken image, alt text is the entire content. It is the fallback path, not only the accessibility path (Offline UX).
- Correct responsive markup is verbose. A
<picture>with three formats and four widths is a lot of characters for one image, and it wants to be generated by a component or a CDN rather than written by hand. - Declaring intrinsic dimensions couples markup to the asset. When the asset is replaced with a different aspect ratio and the attributes are not updated, the reserved box is now the wrong shape.
- Modern formats reduce bytes and cost encoding time, storage for multiple variants, and a fallback path to maintain (CDN Delivery).
- Inline SVG is styleable and accessible and adds nodes to the DOM, the style pass and the accessibility tree. A page with sixty inline icons has paid for sixty subtrees (Selector Matching Cost).
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALReplaced-element sizing, the aspect ratio derived from
widthandheightattributes,srcset/sizescandidate selection and<picture>source ordering are specified behaviour implemented across Blink, Gecko and WebKit. - BROWSER-SPECIFICWhich
srcsetcandidate is chosen is explicitly left to the browser: implementations may consider cache state, network conditions and their own heuristics, so two engines on the same device can pick different files. Never write code that assumes a specific candidate was selected — readcurrentSrcinstead. - DEVICE-SPECIFICDecode memory scales with pixel count and is bounded by device memory, so the same page is comfortable on a laptop and evicted on a mid-range phone. Hardware video decode availability also differs by device and codec, which changes whether a background video is nearly free or a constant CPU load.
- SPEC-EVOLVINGAutoplay policies, format support (AVIF, WebP, HEIC) and lazy-loading heuristics change on vendor schedules rather than specification ones. Treat any specific rule about when autoplay is permitted as a snapshot and always handle the rejected
play()promise.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Runtime Internals — why decode memory is charged to the renderer heap in units of pixels rather than bytes on the wire, and what the garbage collector can and cannot reclaim from a detached image.