Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Advanced+200 XP

Responsive Images & Performance

srcset, sizes, picture, and the image-loading techniques that keep a real page fast on real connections.

35 min August 2026
// Part 01 — The Problem With One Image For Everyone

Why a Single Image File Is Almost Always Wrong

A single <img src="..."> points at exactly one file, and that one file has to serve every device that requests it — a laptop with a 1x display sitting on a fast office connection, a five-year-old Android phone on a throttled mobile connection, and a 3x-density iPhone in the same browsing session. If the image is sized and compressed for the laptop, the phone downloads far more data than its small screen can even display. If it is sized for the phone, the laptop shows a soft, upscaled image. There has never been one correct file size for every device — which is exactly the problem srcset, sizes, and <picture> exist to solve.

It helps to separate two distinct problems that get conflated constantly, because the browser solves them with two different tools:

Two different problems, two different tools
1. RESOLUTION SWITCHING
   Same crop, same content, different FILE SIZES for different screen
   densities and viewport widths. Tool: srcset + sizes on a single <img>.

2. ART DIRECTION
   Genuinely different CROPS or compositions for different screen sizes —
   a tight portrait crop on mobile, a wide landscape crop on desktop.
   Tool: <picture> with multiple <source> elements.

This module covers both in depth, then layers on the format question (WebP/AVIF vs JPEG/PNG) and the two Core Web Vitals metrics — CLS and LCP — that images are the single most common cause of failing.

💡 Note
This module assumes you have already been through Images and Media, where img, alt, width/height, and a first pass at loading="lazy" were covered. Everything here builds directly on that foundation rather than repeating it.
// Part 02 — srcset and sizes

Resolution Switching — Serving the Right File Size Automatically

srcset gives the browser a list of candidate image files, each labeled with either its pixel width or its pixel density, and lets the browser — not your CSS, not JavaScript — choose which one to actually download, based on the real device it is running on and the real size the image will render at.

Density descriptors — the simple case

The simplest form of srcset offers the same image at 1x, 2x, and sometimes 3x pixel density, for a fixed display size (a logo or an icon that always renders at a known CSS size regardless of viewport).

Density descriptors — for a fixed-size image
<img
  src="/logo-200.png"
  srcset="/logo-200.png 1x, /logo-400.png 2x, /logo-600.png 3x"
  alt="Chaduvuko logo"
  width="200"
  height="60"
>

<!-- A standard 1x display downloads logo-200.png.
     A Retina-class 2x display downloads logo-400.png.
     A 3x phone display downloads logo-600.png.
     The browser measures its own pixel density and picks automatically. -->

Width descriptors — the case that actually matters for real content images

Density descriptors only work when the image renders at a fixed, known CSS size. Most real content images — a blog hero, a product photo, a card thumbnail — render at a size that changes with the viewport (full-width on mobile, a third of the width on desktop). For those, srcset uses width descriptors instead — labeling each candidate with its actual intrinsic pixel width using w, not a density multiplier.

Width descriptors — multiple real file sizes offered
<img
  src="/blog/hero-800.jpg"
  srcset="
    /blog/hero-400.jpg   400w,
    /blog/hero-800.jpg   800w,
    /blog/hero-1200.jpg 1200w,
    /blog/hero-1600.jpg 1600w
  "
  sizes="(max-width: 600px) 100vw, (max-width: 1000px) 50vw, 800px"
  alt="Team standup meeting around a whiteboard"
  width="1200"
  height="675"
>

Width descriptors alone are not enough — the browser also needs to know how large the image is actually going to be rendered, in CSS pixels, at the current viewport width, before it can pick the smallest candidate that is still large enough. That is exactly what sizes supplies.

sizes — telling the browser the rendered width, before it downloads anything

sizes is a comma-separated list of media conditions paired with a rendered width, evaluated top to bottom — the browser uses the first condition that matches the current viewport. The final, condition-less value is the fallback for anything that did not match.

Reading sizes step by step
sizes="(max-width: 600px) 100vw, (max-width: 1000px) 50vw, 800px"

<!-- Read as three rules, checked in order:
     1. If the viewport is 600px or narrower, this image renders at 100% of
        the viewport width (100vw) — a full-bleed mobile layout.
     2. Else, if the viewport is 1000px or narrower, it renders at 50% of
        the viewport width — a two-column tablet layout.
     3. Otherwise (desktop), it renders at a fixed 800px — the image never
        grows past 800px wide regardless of how wide the screen gets. -->

Combining these two attributes, the browser's decision process is: read sizes to work out how many CSS pixels the image will actually occupy at the current viewport width, multiply that by the screen's pixel density, then pick the smallest candidate from srcset that is still large enough to cover that many real pixels without visibly blurring. It downloads exactly one file — never all of them, and this decision happens before a single byte of any candidate is fetched.

⚠️ Important
sizes is not optional once you use width descriptors. Without it, the browser falls back to assuming the image renders at 100vw — full viewport width — which is very often wrong, and leads to the browser downloading a candidate that is either needlessly large or, worse, too small and visibly soft. Every srcset that uses w descriptors needs a matching sizes attribute that actually reflects your real CSS layout.

The src attribute is still required alongside srcset — it is the fallback used by any browser old enough not to understand srcset at all, and it is also what a browser lazily evaluating the DOM without CSS applied falls back to. Set it to a reasonable mid-size candidate, never the largest file in the set.

// Part 03 — The picture Element

picture and source — True Art Direction, Not Just Resolution Switching

srcset only ever serves the same image at different sizes — same crop, same composition, just more or fewer pixels. Sometimes that is not what a responsive layout actually needs. A wide, cinematic hero photo that looks great spanning a 1600px desktop viewport often becomes an unreadable sliver of background noise when it is naively squeezed down into a 375px-wide mobile hero — the subject shrinks to nothing. What the mobile layout actually needs is a different crop: tighter, portrait-oriented, centered on the subject. That is art direction, and it is a job srcset cannot do — it is exactly what <picture> exists for.

picture with source — different crops for different viewports
<picture>
  <source media="(max-width: 600px)" srcset="/hero/hero-mobile-crop.jpg">
  <source media="(max-width: 1024px)" srcset="/hero/hero-tablet-crop.jpg">
  <img src="/hero/hero-desktop-crop.jpg" alt="Founders reviewing quarterly roadmap on a whiteboard" width="1600" height="700">
</picture>

The mechanics matter here: <picture> is a wrapper element with no rendering of its own. It contains any number of <source> elements followed by exactly one <img>, which is mandatory — not just as a fallback, but because <img> is what actually does the rendering, sizing, and lazy-loading. The browser evaluates each <source>'s media condition top to bottom and uses the first one that matches; if none match, it falls through to the plain <img>'s own src.

⚠️ Important
Every attribute that controls layout — alt, width, height, loading — belongs on the inner img, not on picture or source. <picture> and <source> only ever influence which file gets requested; the <img> is still what the browser lays out, reserves space for, and exposes to assistive technology.

Combining art direction with resolution switching in the same picture

The two techniques are not mutually exclusive — a real production hero image commonly needs both a different crop per breakpoint and multiple resolutions within each crop. Each <source> can carry its own full srcset/sizes pair.

Art direction AND resolution switching, combined
<picture>
  <source
    media="(max-width: 600px)"
    srcset="/hero/mobile-480.jpg 480w, /hero/mobile-960.jpg 960w"
    sizes="100vw"
  >
  <source
    media="(max-width: 1024px)"
    srcset="/hero/tablet-800.jpg 800w, /hero/tablet-1600.jpg 1600w"
    sizes="100vw"
  >
  <img
    src="/hero/desktop-1600.jpg"
    srcset="/hero/desktop-1600.jpg 1600w, /hero/desktop-2400.jpg 2400w"
    sizes="1600px"
    alt="Founders reviewing quarterly roadmap on a whiteboard"
    width="1600"
    height="700"
  >
</picture>

This is genuinely the most complete form the responsive-images system offers: for every breakpoint, the browser both selects the right crop and the right resolution within that crop, downloading exactly one file for the entire element.

// Part 04 — Image Formats

WebP and AVIF vs JPEG and PNG — Choosing (and Falling Back On) a Format

File format is a second, independent axis of optimization, on top of everything srcset and sizes already handle. The same photograph, at the same pixel dimensions, can vary enormously in file size purely based on which compression format encoded it.

Roughly, for the same visual quality
JPEG   — the long-standing default for photos. Lossy, widely supported everywhere.
PNG    — lossless, supports transparency. Best for graphics/icons/screenshots, not photos.
WebP   — typically 25-35% smaller than an equivalent-quality JPEG. Supported in every
          modern browser (Chrome, Firefox, Safari, Edge — all current versions).
AVIF   — typically smaller again than WebP, sometimes significantly. Newer, slightly
          less universally supported, and slower to encode.

The practical answer is almost never "pick one format and use it everywhere" — it is "offer the most efficient format first, and fall back gracefully for anything that cannot decode it," using exactly the same <picture>/<source> mechanism from Part 03, this time keyed off type instead of media.

Format fallback chain — AVIF, then WebP, then JPEG
<picture>
  <source srcset="/products/sneaker.avif" type="image/avif">
  <source srcset="/products/sneaker.webp" type="image/webp">
  <img src="/products/sneaker.jpg" alt="Red and white running sneaker, side profile" width="800" height="800">
</picture>

The browser walks the <source> elements in order and uses the first one whose type it can actually decode — a browser that supports AVIF uses the first line and never even requests the WebP or JPEG files. A browser without AVIF support skips straight past it, tries WebP next, and so on down to the guaranteed universal JPEG fallback. Nothing here requires server-side browser detection or user-agent sniffing — the browser makes the decision itself, based on formats it actually knows how to decode.

🎯 Pro Tip
Format and resolution switching combine in the same picture element. Each <source> can carry its own type AND its own srcset/sizes — a real production image pipeline commonly generates an AVIF set, a WebP set, and a JPEG set, each at several widths, all wired into one <picture>. Tools like Next.js's built-in Image component, or a CDN-based image service, generate this entire matrix automatically from a single source file — hand-writing it is realistic for a handful of hero images, not for every image on a large site.
// Part 05 — loading, fetchpriority, and decoding

loading="lazy" Revisited — And the Priority Attributes Around It

The Images and Media module introduced loading="lazy" as a preview. Here is the performance-focused version: loading is one of three attributes that control how aggressively the browser fetches and decodes an image relative to everything else competing for bandwidth on the page, and getting all three right — or wrong — has a direct, measurable effect on load performance.

The three loading-related attributes
loading="lazy" | "eager"
  Whether the browser defers fetching until the image nears the viewport
  ("lazy") or fetches immediately regardless of position ("eager", the default).

fetchpriority="high" | "low" | "auto"
  A hint about how urgently this specific request should be scheduled
  relative to everything else the page is loading.

decoding="async" | "sync" | "auto"
  Whether decoding the image (turning compressed bytes into pixels) is
  allowed to happen off the main thread, without blocking other rendering.

The rule that matters most in practice: never lazy-load the image that appears above the fold — especially a hero image, which is very often also the page's Largest Contentful Paint element (Part 07). Lazy-loading it actively delays the single metric it is most likely to be judged on, because the browser now waits for a scroll-proximity check that will never meaningfully change anything for an image already in the initial viewport.

A hero image — eager, high priority, synchronous decode
<img
  src="/hero/desktop-1600.jpg"
  alt="Founders reviewing quarterly roadmap on a whiteboard"
  width="1600"
  height="700"
  loading="eager"
  fetchpriority="high"
>

<!-- Everything below the fold: the opposite treatment -->
<img
  src="/blog/footer-illustration.png"
  alt="Illustration of a rocket launching"
  width="800"
  height="400"
  loading="lazy"
  fetchpriority="low"
>
⚠️ Important
loading="lazy" and fetchpriority="high" are contradictory on the same image. Setting both tells the browser to both defer the request and treat it as urgent — browsers generally resolve this by not lazy-loading at all, but the underlying mistake is real: pick one intent per image based on whether it is visible on first paint, not both.
// Part 06 — CLS

Cumulative Layout Shift — Images Are the Most Common Cause

Cumulative Layout Shift (CLS) is one of Google's three Core Web Vitals, and it measures exactly one thing: how much visible content unexpectedly moves after it has already been painted to the screen. It is scored, not binary — every unexpected shift contributes a fraction to a running total for the page, based on how much of the viewport moved and how far it moved.

The Images and Media module already introduced the mechanism: an <img> with no width/height (and no equivalent CSS aspect-ratio) gives the browser zero information about how tall it will be before the file downloads. The browser renders the page with no space reserved, then — the instant the image's real dimensions become known — shoves everything below it downward. That sudden shove is precisely what CLS measures.

The CLS-causing pattern, one more time, with the fix
<!-- Causes layout shift: no dimensions, no reserved space -->
<img src="/blog/hero.jpg" alt="Solar panels on a hillside at sunset">

<!-- Fixed: width/height give the browser the aspect ratio immediately -->
<img
  src="/blog/hero.jpg"
  alt="Solar panels on a hillside at sunset"
  width="1200"
  height="675"
>

It is worth being precise about what width/height actually give the browser: not a fixed pixel size, but a ratio. Modern browsers compute an implicit aspect-ratio from the two attributes and reserve exactly that shape of space, then let CSS control the final rendered size on top of it — which is why img { max-width: 100%; height: auto; } and explicit width/height attributes work together rather than conflicting.

Other common CLS sources images introduce, beyond a missing width/height

Two more image-related CLS triggers
1. Web fonts swapping in after a font-based icon or a text label reflows
   the layout around a nearby image — not the image itself, but often
   diagnosed alongside it since both show up in the same DevTools report.

2. An ad slot or embed placeholder that has no reserved height and pops
   in above or beside an image once its content loads asynchronously.
🎯 Pro Tip
You do not need to guess at your CLS score — Chrome DevTools measures it directly. The Performance panel's Experience section flags individual layout shift events with a red bar, and clicking one highlights exactly which element moved and by how much. Lighthouse (built into DevTools, under the Lighthouse tab) reports an aggregate CLS score for the whole page load, scored against Google's published thresholds: under 0.1 is "good," 0.1–0.25 is "needs improvement," and above 0.25 is "poor."
// Part 07 — LCP

Largest Contentful Paint — Why the Hero Image Is Usually the Bottleneck

Largest Contentful Paint (LCP) measures how long it takes the single largest visible element — on most real pages, this is a hero image, a large heading, or a background image — to finish rendering after the page starts loading. It is the Core Web Vital most directly tied to a user's felt sense of "is this page actually here yet," and on the overwhelming majority of content-heavy pages, the LCP element is an image.

An unoptimized hero image drags LCP down through a predictable, stacked sequence of delays — each one independently fixable with a technique already covered earlier in this module.

Where the delay actually comes from, stacked
1. Oversized file — a 4000px-wide, unresized original JPEG serving a
   1600px hero slot. Fix: srcset with correctly sized candidates (Part 02).

2. Wrong format — an uncompressed PNG for what is fundamentally a photo.
   Fix: WebP/AVIF with a JPEG fallback (Part 04).

3. Lazy-loaded above the fold — deferring the fetch of the very element
   LCP is measuring. Fix: loading="eager" + fetchpriority="high" (Part 05).

4. Discovered late — the image is only referenced inside a CSS
   background-image rule in an external stylesheet, so the browser cannot
   even start the request until the CSS has downloaded and parsed. Fix:
   use a real <img> (or <picture>) in the HTML for LCP candidates, not
   a CSS background — the browser's preload scanner can discover an
   <img>'s src while still parsing HTML, well before CSS is involved.
⚠️ Important
A background-image hero is one of the most common, least obvious LCP mistakes. Because it lives in CSS rather than HTML, the browser's HTML preload scanner — the mechanism that starts fetching images it finds while still parsing the document — never sees it. The request only begins once the relevant stylesheet has downloaded and been parsed, which can add hundreds of milliseconds of pure delay to your LCP element for no visual reason at all.

Google's published LCP thresholds: under 2.5 seconds is "good," 2.5–4.0 seconds is "needs improvement," and above 4.0 seconds is "poor" — measured from navigation start, on real-user data collected across actual visits, not just a single synthetic lab test. Lighthouse gives you a lab-based estimate immediately; PageSpeed Insights and the Chrome UX Report show the real-user field data your actual visitors experienced.

// Part 08 — Real World
💼 What This Looks Like at Work

A Portland Furniture Retailer's LCP Goes From 6.1s to 1.8s

Scenario — Furniture e-commerce, Portland · Core Web Vitals audit

An online furniture retailer in Portland notices their product listing pages have quietly slipped out of Google's "good" Core Web Vitals bucket, coinciding with a real, measurable drop in organic search traffic to those exact pages. The design team had recently shipped a redesigned category hero — a large, moody lifestyle photograph behind the category title — and nobody connected the two events until an engineer ran Lighthouse directly against a product listing page.

What the audit finds

The report flags an LCP of 6.1 seconds, with the hero photograph identified as the LCP element. Digging into the Network panel, three separate, independently diagnosable problems stack on top of each other:

The original hero markup
<div class="category-hero" style="background-image: url('/hero/category-bg-original.jpg')">
  <h1>Living Room Furniture</h1>
</div>

First, the image is a single 4200×1800 JPEG straight out of the photographer's export, weighing 3.8MB, serving a hero slot that never renders wider than 1600px on any real device — the exact single-file problem from Part 01. Second, it is set as a CSS background-image, invisible to the HTML preload scanner, adding roughly 400ms of pure discovery delay before the request even starts, exactly the mistake called out in Part 07. Third — and this one had been true even before the redesign — the surrounding product grid images have no width/height attributes at all, so every page load also carries a poor CLS score as the grid visibly jumps once each thumbnail's real dimensions resolve.

The fix

The rebuilt hero — real img, srcset, format fallback, correct priority
<picture class="category-hero">
  <source
    srcset="/hero/category-bg-800.avif 800w, /hero/category-bg-1600.avif 1600w"
    sizes="100vw"
    type="image/avif"
  >
  <source
    srcset="/hero/category-bg-800.webp 800w, /hero/category-bg-1600.webp 1600w"
    sizes="100vw"
    type="image/webp"
  >
  <img
    src="/hero/category-bg-1600.jpg"
    alt="Modern living room with a grey sectional sofa and warm ambient lighting"
    width="1600"
    height="500"
    loading="eager"
    fetchpriority="high"
  >
</picture>
<h1>Living Room Furniture</h1>

Alongside the hero fix, the engineer runs a bulk pass adding correct width/height to every product grid thumbnail, and adds loading="lazy" to everything below the third row, which was previously loading eagerly for no reason. LCP drops from 6.1s to 1.8s — well inside Google's "good" threshold — and CLS drops from 0.34 ("poor") to 0.02 ("good"). Organic traffic to the affected pages recovers over the following search index cycle. Nothing about the visual design changed at all; every fix here was purely about how the same images were delivered.

// Part 09 — Misconceptions

Four Misconceptions About Responsive Images

✕ ""srcset downloads every candidate and the browser picks the best-looking one afterward""
The browser evaluates sizes and its own viewport/density before requesting anything, then downloads exactly one candidate from srcset. It never fetches multiple files to compare — the decision happens ahead of any network request.
✕ ""picture and srcset solve the same problem — picture is just the newer syntax""
They solve different problems. srcset is resolution switching — the same crop at different sizes. picture with source is art direction — genuinely different crops or formats per condition. Many production images legitimately need both at once.
✕ ""WebP/AVIF need a JavaScript fallback for browsers that don't support them""
No JavaScript is involved at all. The <picture>/<source type="..."> fallback chain is resolved entirely by the browser's own format-decoding capability — it simply skips any <source> it cannot decode and falls through to the next one, down to the universal <img> fallback.
✕ ""Core Web Vitals are just a Google ranking gimmick, not a real performance signal""
CLS and LCP measure genuinely real, user-felt problems — content jumping around unexpectedly, and how long the main visible content takes to appear. They affect search ranking specifically because they correlate with real user experience; fixing them is worth doing even ignoring SEO entirely.
// Part 10 — Interview Prep

5 Interview Questions — With Complete Answers

What is the difference between srcset with density descriptors and srcset with width descriptors?
Density descriptors (1x, 2x, 3x) are for a fixed-size element — the same rendered CSS size across all viewports, just swapping which file gets used based on screen pixel density. Width descriptors (400w, 800w, ...) describe each candidate's real intrinsic pixel width and require a matching sizes attribute, because the browser needs to know how large the image will actually render at the current viewport before it can pick a candidate large enough to look sharp without being wasteful.
When would you reach for picture with source instead of a plain img with srcset?
When the responsive requirement is art direction, not just resolution — genuinely different crops or compositions per breakpoint (a tight portrait crop on mobile vs a wide landscape crop on desktop), or serving different image formats (AVIF/WebP/JPEG) with a fallback chain. srcset alone can only offer different sizes of the exact same crop.
How do width and height attributes actually prevent layout shift, given that responsive images also need to scale fluidly with CSS?
The two attributes give the browser an aspect ratio, not a fixed pixel size — modern browsers compute an implicit aspect-ratio from them and reserve exactly that shape of space in the layout before the file has downloaded. CSS like max-width: 100%; height: auto; then controls the actual rendered size on top of that reserved space. The attributes and the fluid CSS solve two different problems and are meant to be used together, not as alternatives.
Why might a background-image CSS hero hurt LCP more than an equivalent img element, even with identical file size and format?
The browser's HTML preload scanner discovers <img> src (and <source> srcset) attributes while still parsing the raw HTML, and can start the image request immediately. An image referenced only inside a CSS background-image rule is invisible to that scanner — the request cannot begin until the relevant stylesheet has been downloaded and parsed, adding pure discovery delay before the fetch even starts, directly hurting LCP if that image is the page's largest visible element.
You are told loading="lazy" was recently added to a page's hero image and LCP got worse. Why would that happen, and what's the fix?
loading="lazy" defers the fetch until the browser determines the image is nearing the viewport based on scroll proximity — but a hero image is already inside the initial viewport on page load, so the deferral adds pure, unnecessary delay to the single element LCP is measuring, since there is no scroll to wait for. The fix is loading="eager" (or simply omitting loading, since eager is the default) combined with fetchpriority="high" specifically for above-the-fold images; reserve loading="lazy" for images genuinely below the fold.
// Common Mistakes

Responsive Image Mistakes Made Constantly, Even by Experienced Engineers

Using srcset with width descriptors but forgetting sizes entirely
Without sizes, the browser assumes the image renders at 100vw — often wrong — and the whole point of offering multiple width candidates is undermined by a guess the browser had no choice but to make.
Putting layout-affecting attributes on picture or source instead of the inner img
alt, width, height, loading, and fetchpriority only matter on the <img> — picture and source only ever influence which file gets requested. Attributes placed on the wrong element are silently ignored.
Lazy-loading the hero image because "lazy loading is always good practice"
loading="lazy" on an above-the-fold, likely-LCP image actively delays your Largest Contentful Paint. Reserve it for images genuinely below the initial viewport.
Offering only a single JPEG fallback with no WebP/AVIF sources at all
This leaves real, easy file-size savings on the table for every modern browser, which is the overwhelming majority of real traffic — the fallback chain costs nothing for browsers that do support the newer formats.
Fixing CLS on the hero image but leaving every other image on the page without width/height
CLS is scored across the entire page, not just the LCP element — a product grid or card list full of undimensioned images can independently tank the same score the hero fix was meant to improve.
// Error Library

Warnings and Rendering Bugs Responsive Images Actually Produce

Image with src '/hero.jpg' has intrinsic size 4200x1800 but rendered size 1600x686
Cause: A Lighthouse/DevTools warning meaning the browser downloaded and decoded a much larger file than the space it actually occupies on the page — wasted bandwidth and decode time with zero visual benefit.
Fix: Generate correctly sized candidates and wire up srcset/sizes (or a picture element) so the browser can choose a file close to its real rendered size instead of one oversized original.
sizes attribute has an invalid value and was ignored
Cause: A typo or malformed media condition inside sizes — commonly a missing comma between entries, or a media query written without parentheses.
Fix: Check each comma-separated entry follows the exact pattern (media-condition) width, with the final fallback entry having no condition at all.
The image is missing a source with type image/webp or similar Lighthouse "Serve images in next-gen formats" flag
Cause: Only a JPEG/PNG source is offered, with no WebP or AVIF candidate, so every visiting browser downloads the larger legacy-format file even when it could have decoded a smaller modern one.
Fix: Add WebP and/or AVIF <source> entries ahead of the JPEG/PNG fallback inside a <picture> element, generated from the same original image.
Cumulative Layout Shift flagged in the DevTools Performance panel, image element highlighted red
Cause: An image (or a group of images, such as a product grid) has no width/height attributes or CSS aspect-ratio, so the browser reserves no space before the file loads and shifts everything below it once the real dimensions are known.
Fix: Add width and height attributes (matching the real aspect ratio) to every image, or set an explicit CSS aspect-ratio on the element if the dimensions are not known ahead of time.
Largest Contentful Paint element flagged as a background-image, not discoverable by preload scanner
Cause: The page's largest visible element is set via a CSS background-image rule rather than an <img>/<picture>, so the browser cannot begin fetching it until the relevant stylesheet has downloaded and been parsed.
Fix: Replace CSS-only hero backgrounds with a real <img> (or <picture>) in the HTML wherever that image is a realistic LCP candidate, so the preload scanner can discover and prioritize it immediately.

🎯 Key Takeaways

  • srcset + sizes solve resolution switching — the same crop, different file sizes, chosen automatically by the browser before any file is requested.
  • picture + source solve art direction — genuinely different crops, compositions, or formats per condition, something srcset alone cannot express.
  • sizes is not optional when using width descriptors (w) — without it the browser assumes 100vw, which is very often wrong for real layouts.
  • Offer WebP/AVIF ahead of a JPEG/PNG fallback inside a picture element — the browser resolves the fallback chain natively, with no JavaScript involved.
  • width and height (or CSS aspect-ratio) give the browser a ratio to reserve layout space with before the file loads — this is what prevents CLS, and it works alongside fluid, responsive CSS sizing rather than against it.
  • The LCP element on most real pages is an image — usually a hero. Oversized files, wrong formats, unnecessary lazy-loading, and CSS-only background images all independently delay it.
  • Never lazy-load an above-the-fold image, especially a likely LCP candidate — use loading="eager" and fetchpriority="high" instead, and reserve loading="lazy" for content genuinely below the fold.
  • A CSS background-image is invisible to the browser's HTML preload scanner — a real img/picture in the HTML lets the browser discover and prioritize it far earlier.

What comes next

Module 38 turns to accessibility at the CSS level — visible focus states, WCAG color contrast ratios, respecting prefers-reduced-motion, and designing hover interactions that still work on touch devices with no true hover state.

Module 38 → CSS Accessibility Best Practices
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...