A responsive image is a contract between the file you ship and the box it renders into. Linters can check that the syntax is valid; they cannot tell you the sizes value matches the real layout, that the intrinsic-width labels are honest, or that the LCP image loads first. This lens reviews that judgment: role-based sizing (hero vs grid cell vs thumbnail), load priority, and which candidate the browser actually downloads.
Checklist (lead with the trap)
w-descriptorsrcsetwith nosizes-> the browser assumes100vwand downloads the widest candidate that fits the full viewport. A 400px thumbnail withsrcset="...1600w"and nosizesfetches the 1600w file on a wide screen: wasted bytes and, if it is the LCP element, a slower paint. Addsizesdescribing the real rendered width.sizesmust match the actual CSS layout, not a guess.sizes="100vw"on an image that renders in a 3-column grid (really ~33vw) over-downloads ~3x; a too-smallsizesunder-downloads and renders soft.sizesis a media-condition list read left-to-right, first true wins, with a bare fallback length last. Measure the rendered width across breakpoints; do not eyeball it.- Intrinsic-width descriptors must equal the file's real decoded width.
photo-1200.jpg 640wwhen the file is 1200px poisons the selection math — the browser thinks 640w is enough and serves blur, or the opposite. Verify eachw/xlabel against the actual pixel width of the file it points at. - The hero/LCP image must not be
loading="lazy". Lazy delays discovery and typically regresses LCP. Useloading="eager"(or omitloading) plusfetchpriority="high"so it wins network priority from discovery. Capfetchpriority="high"at 1-2 elements per page — if everything is high, nothing is — and keep genuinely below-the-fold imagesloading="lazy". - Every
<img>needswidth+heightattributes (or a reservedaspect-ratio) or it shifts layout when the file arrives (CLS). An image sized only in CSS (width: 100%; height: auto) with nowidth/heightattributes collapses to height 0 until it downloads, then shoves the content below it down the page. Keep the attributes so the browser derives anaspect-ratioand reserves the box up front, and pair them with CSSmax-width: 100%; height: autoto stay responsive. Do not override with a fixed CSSheight(it distorts the ratio) or drop the attributes (the collapse returns). - Too few candidates. One or two widths in
srcsetcannot serve DPR 2-3 phones and wide desktops well; provide a spread that brackets the realsizesrange. Do not over-file: a fixed-size icon or logo needs nosrcsetat all. - Descriptor type must match intent.
x(density) descriptors are for a fixed display size across DPRs;w(width) descriptors +sizesare for variable layout width. Mixing them, or addingsizesto anx-descriptorsrcset(where it is ignored), is a misuse —sizesis only meaningful withw. <picture>is for art-direction and format fallback, not resolution switching. Use<source media="...">when the crop/aspect genuinely changes per breakpoint, and<source type="image/avif">before webp/jpg for format fallback. Each<source>needs itsmediaortype, and a real<img src alt>must close the<picture>. If only the resolution changes, drop<picture>and putsrcset/sizeson the<img>.sizes="auto"is only valid withloading="lazy". On an eager image the value is invalid and browsers fall back to100vw(over-download). Keep explicitsizeslengths for eager/hero images; only useautoon lazy ones, and keepwidth/heightfor the aspect ratio.
Quick probes
Leads, not proof — HTML/JSX tags can span multiple lines, so confirm on the rendered tag before filing. Mechanical validity belongs to the linters below; use these to find candidates to judge.
# w-descriptor srcset with no sizes on the same line -> 100vw over-download
rg -n '<img[^>]*srcset=' src/ app/ components/ 2>/dev/null | rg -v 'sizes='
# <img> tags to eyeball for missing width/height (CLS)
rg -n '<img\b' src/ app/ components/ 2>/dev/null | rg -v 'width='
# density (x) vs width descriptors, <picture> sources, and sizes=auto
rg -n 'srcset=[^>]*[0-9]x|<source\b|sizes="auto"' src/ app/ components/ 2>/dev/null
# LCP/hero must not be lazy; below-fold should be
rg -n 'loading="(lazy|eager)"|fetchpriority' src/ app/ components/ 2>/dev/null
Run the mechanical linters first — they own syntax so this lens can own judgment. RespImageLint is a bookmarklet that resizes the page across viewports, measures each image's real rendered width, and prints the correct sizes/srcset. The W3C Nu Html Checker flags the mechanical conformance violations in CI (it has an API) — sizes without srcset, a w-descriptor srcset with no sizes, or sizes="auto" on a non-lazy image — and Markuplint's invalid-attr rule checks each srcset/sizes/loading value against the HTML standard. eslint-plugin-layout-shift fails the build on media elements missing width/height.
Boundary with sibling skills
- Prior art / route the mechanical checks out. The mechanical
srcset/sizes/width-heightconstraints are already enforced by linters — RespImageLint (bookmarklet), the W3C Nu Html Checker and Markuplint'sinvalid-attrrule (srcset/sizes/loadingconformance), and eslint-plugin-layout-shift (require width/height). Send "is the syntax valid / is width/height present" there. This skill owns the review judgment those tools cannot make: role-based sizing (hero vs thumbnail vs grid cell), the LCP eager/fetchprioritydecision, whethersizesmatches the actual CSS layout, and diagnosing which candidate the browser really downloaded (DevTools -> the element'scurrentSrc, or the actual URL in the Network panel; on Chromium with a serverAccept-CHopt-in, theSec-CH-Widthrequest hint). - design-to-code-fidelity — for rendered-vs-design visual/pixel drift (wrong crop, visible blur, off dimensions treated as a visual mismatch), not the
srcsetselection contract. - i18n-copy-and-layout — only for localized caption, filename, or
altcopy display, not image selection. - LCP scope. This lens owns image-load priority for the LCP element (eager +
fetchpriority, discoverable in the HTML). Whole-page Core Web Vitals budgeting — JS/CSS, fonts, TTFB, non-image LCP — is out of scope.
PR-worthiness gate
Count a finding only when all are true:
- Real user-visible cost: extra bytes downloaded, a soft/blurry render, a layout shift, or a delayed LCP — not merely non-idiomatic markup.
- The declared contract provably disagrees with the layout or role:
sizesvs measured rendered width, descriptor vs actual file width,loading="lazy"on the LCP element, missing space reservation. - A narrow fix exists: one
sizesvalue, oneloading/fetchprioritychange, onewidth/heightpair, or a<picture>/<img>swap. - The mechanical linters do not already catch and gate it.
Reject weak findings:
- Valid syntax the linter already owns, with no measured over/under-download.
- A fixed-size icon, logo, or sprite that legitimately needs no
srcset. fetchpriority="high"absent on a genuinely below-the-fold image (correct).sizes="100vw"on an image that really does span the viewport (a true hero/banner).- A framework image component (
next/image,@astrojs/image, a CDN loader) that computessizes/srcset— inspect the generated output before claiming a defect.
Output shape
Return compact findings:
- Contract: sizes-vs-layout / descriptor-vs-file / LCP priority / CLS reservation / picture-vs-srcset / sizes=auto.
- Evidence: file:line, the real rendered width or image role, and the candidate actually downloaded (
currentSrc, or the requested URL in the Network panel) when available. - Cost: wasted bytes, blur, CLS, or LCP delay.
- Fix: the smallest attribute change.
- Verification: re-run RespImageLint or re-check
currentSrcat the target viewport, or a Lighthouse/DevTools LCP-discovery check.
Sources
- MDN, Using responsive images in HTML — resolution switching (
srcset/sizes,wvsxdescriptors) vs art direction, and the selection math. - MDN,
HTMLImageElement.sizesand.srcset—sizesdefaults to100vw, is only meaningful withwdescriptors, and theautokeyword requiresloading="lazy". - Web Platform features,
<img sizes="auto">—sizes="auto"applies only toloading="lazy"images; otherwise browsers fall back to100vw. - MDN,
<picture>—media/typeon<source>, the mandatory<img>fallback, and when to prefersrcsetover<picture>. - web.dev, Browser-level image lazy loading and Fetch Priority API — never lazy-load the LCP image; use
fetchpriority="high"on 1-2 resources. - Chrome for Developers, LCP request discovery and MDN, Fix image LCP — make the LCP image discoverable,
fetchpriority="high", and notloading="lazy". - web.dev, Optimize Cumulative Layout Shift — set
width/heightattributes oraspect-ratioto reserve space; keepheight: auto; max-width: 100%in CSS so images sized only in CSS do not collapse to height 0. - Tools: RespImageLint, W3C Nu Html Checker and Markuplint rules (
invalid-attr), eslint-plugin-layout-shift (require-size-attributes).