Frontend / Checklist

Images are the heaviest bytes you ship - handle them like it

Images are the largest share of most pages' transfer size and the usual Largest Contentful Paint element, so they decide the performance score before a line of JavaScript runs. These rules cover the attributes every img needs, the format decision, the sizing pipeline, and the alt text that makes the picture count for everyone.

20 rules in 5 groups. Adapted from the Front-End Checklist (David Dias, MIT), rewritten and re-verified September 2026.

What this checklist decides

Whether every image on the page carries the attributes that stop layout shift and wasted bytes, whether the largest one is fetched first, and whether the format and pixel size match what the slot actually displays. Almost all of it is markup you can verify by reading the built HTML.

The source lists 25 image rules in six groups. Three format rows that said the same thing are one row here, three compress-and-optimize rows are one, the two srcset rows are one, and the one-rule Loading group is folded into Performance. That leaves 20 rules in five groups, each naming the attribute, header or tool that satisfies it.

Accessibility

Two rules, one of them a ship-blocker: the text that stands in for the image, and the caption that belongs to it.

Rule Priority What to do
Give every image alt text that carries its meaning Critical Every <img> has an alt attribute. Describe what the image contributes to the page, not what it looks like, and give purely decorative images alt="" so screen readers skip them. A missing attribute gets the filename read aloud.
Attach visible captions with figure and figcaption Medium Wrap an image that has a caption in <figure> with a <figcaption>, so the caption is programmatically tied to the image instead of being a nearby paragraph. The alt still describes the image; the caption adds the context around it.

Both rules on one content image, and the decorative case that trips people up:

<figure>
  <img src="/img/pipeline.png" width="1200" height="675"
       alt="Deploy pipeline with build, test, preview and release stages, all passing">
  <figcaption>The release pipeline after the preview stage was added.</figcaption>
</figure>

<!-- decorative: the alt is empty, never omitted -->
<img src="/img/divider.svg" width="320" height="8" alt="">

Gotcha: alt text that repeats the caption gets read twice. If the caption already says everything, the alt should cover what the caption does not - or the image is decorative and gets alt="".

Performance

The source keeps a one-rule Loading group for the LCP image; it is folded in here as the first row, because it is the same decision made from the other direction - what to load first, then what not to load at all.

Rule Priority What to do
Preload the LCP image and raise its priority High Find the largest above-the-fold image (usually the hero), add <link rel="preload" as="image"> for it in the head and fetchpriority="high" on the <img>. Never give that one loading="lazy"; that alone pushes the LCP request behind layout.
Set width and height on every img High The width and height attributes hand the browser the aspect ratio before a byte arrives, so the slot is reserved and nothing shifts. Keep height: auto in CSS so the image still scales with its column.
Lazy-load everything below the fold High loading="lazy" on any image outside the first viewport defers the request until the user scrolls near it; pair it with decoding="async" so decoding never blocks the main thread. Native, no library, no observer code.
Serve the pixels the slot displays High A 2400px master in a 400px column is bytes the user downloads and the browser throws away. Generate each rendered size at build time (sharp, Astro's image pipeline) or through an image CDN, and reference the rendition that matches the layout.
Let the browser choose with srcset and sizes High Width descriptors in srcset (hero-800.png 800w, hero-1600.png 1600w) plus a sizes attribute that mirrors the CSS layout. Without sizes the browser assumes the image is viewport-wide and fetches the largest candidate on every phone.
Keep each image inside a byte budget High Set a per-image ceiling the build enforces (a Playwright assets test that fails on outliers, or a size check in the image pipeline) and treat any single image heavier than the page's whole CSS and JS as a defect. The exact number matters less than having one that breaks the build.

The first row in practice - the preload lives in the head, and the img in the body names the same candidates so the preloaded bytes are reused:

<!-- head -->
<link rel="preload" as="image" fetchpriority="high"
      href="/img/hero.png"
      imagesrcset="/img/hero-800.png 800w, /img/hero-1600.png 1600w"
      imagesizes="100vw">

<!-- body: identical srcset and sizes, eager (the default), high priority -->
<img src="/img/hero.png"
     srcset="/img/hero-800.png 800w, /img/hero-1600.png 1600w"
     sizes="100vw" width="1600" height="900"
     fetchpriority="high" decoding="async"
     alt="Product dashboard on a laptop, showing the weekly usage chart">

Gotcha: a preload only helps when the <link> and the <img> resolve to the same URL. Different candidates or a different sizes value means the image downloads twice, and the console warns about an unused preload a few seconds after load.

Formats

The source spends three rows saying "use WebP and AVIF"; that is one decision here, made per image type, with the fallback element as its own rule because it is the part people skip.

Rule Priority What to do
Wrap modern formats in picture with an img fallback High <picture> with one <source type="image/avif">, one <source type="image/webp">, and the <img> last. The browser takes the first type it can decode; the img carries the alt, width, height, loading and the PNG fallback, so nothing is lost where AVIF is missing.
Pick the format by what the image is High AVIF or WebP for photographic content, PNG stays right for flat-color UI art and screenshots, SVG for icons, always with a <picture> + <img> fallback. A screenshot re-encoded as lossy WebP smears text edges for a trivial saving; a photo stored as PNG multiplies its weight for nothing.
Encode any JPEG you still ship as progressive Low If legacy uploads leave JPEGs in the pipeline, progressive encoding renders a coarse full frame first instead of filling top to bottom. Worth a flag in the compressor; not worth keeping JPEG for.

A complete responsive picture. The format paths use Cloudflare's URL transformations; a build step that writes one file per format works exactly the same way:

<picture>
  <source type="image/avif"
          srcset="/cdn-cgi/image/format=avif,width=800/img/team.png 800w,
                  /cdn-cgi/image/format=avif,width=1600/img/team.png 1600w"
          sizes="(min-width: 64rem) 50vw, 100vw">
  <source type="image/webp"
          srcset="/cdn-cgi/image/format=webp,width=800/img/team.png 800w,
                  /cdn-cgi/image/format=webp,width=1600/img/team.png 1600w"
          sizes="(min-width: 64rem) 50vw, 100vw">
  <img src="/img/team.png"
       srcset="/cdn-cgi/image/width=800/img/team.png 800w,
               /cdn-cgi/image/width=1600/img/team.png 1600w"
       sizes="(min-width: 64rem) 50vw, 100vw"
       width="1600" height="1067"
       loading="lazy" decoding="async"
       alt="The support team at the Austin office, standing in front of the status wall">
</picture>

Gotcha: <source> inside a picture ignores src - it needs srcset, and every source needs its own sizes. Order matters too: the browser stops at the first source it can decode, so AVIF goes above WebP or Chromium never sees it.

Optimization

The pipeline between the designer's export and the bytes on the wire. The three source rows that all said "compress and optimize" are one row here, so eight remain.

Rule Priority What to do
Fail the build on a broken image High An image that returns 404 is a layout hole, a console error and a wasted request. Crawl the built output (a Playwright assets spec or a link checker) and assert every src and srcset candidate resolves before anything deploys.
Serve through an image CDN High An image CDN (Cloudflare Images, Cloudinary, imgix) resizes, converts and caches per request, so you store one master and stop committing six renditions. On Cloudflare that is the /cdn-cgi/image/ URL prefix in front of the original path and no build step.
Compress and strip metadata before anything lands in the output High Run every raster through a compressor in the build - sharp, oxipng or pngquant for PNG, the AVIF and WebP encoders at a tuned quality - and drop EXIF, ICC and embedded thumbnail data. Nothing uncompressed reaches the output directory.
Keep inline SVG to icons Medium Inline SVG is HTML weight on every page and is never cached on its own. Inline the icons you style with CSS; load illustrations and logos through <img> or an SVG sprite with <use> so they are fetched once and cached.
Run SVGO on every SVG Medium svgo removes editor metadata, comments, hidden layers and excess path precision. Keep the viewBox and any id your CSS or <use> references; leave fixed width and height in place on anything loaded through an img.
Handle a failed load gracefully Low Give the slot a background and alt text that reads as a sentence, so a failed request degrades to readable content instead of a broken-image icon. A delegated error listener can swap in a placeholder where the image is load-bearing.
Name files for what they show Low office-austin-team.png beats IMG_4021.png for image search, for the CDN log, and for the next developer grepping the repo. Lowercase, hyphens, no spaces.
Sprite only when request count is the bottleneck Low HTTP/2 multiplexing removed most of the case for raster sprites. An SVG sprite (<symbol> plus <use>) still pays for an icon set, because one cached file serves every icon on every page.

Gotcha: compressing an already-compressed image again costs quality and saves almost nothing. Compress once, from the master, in the build - never from whatever is already sitting in the output directory.

Responsive

One rule is left here after srcset moved up to Performance: the density question, which only applies to images that do not change size with the layout.

Rule Priority What to do
Ship 2x candidates for high-DPI screens Medium Fixed-size images (logos, avatars, icons that are not SVG) use density descriptors: srcset="logo.png 1x, [email protected] 2x". Fluid images already get the right density from width descriptors plus sizes. A 3x candidate is for tiny logos where the bytes are trivial; a 3x photo is waste on every screen.

Gotcha: density descriptors and width descriptors cannot share one srcset. Mixing 2x with 800w makes the whole attribute invalid and the browser silently falls back to src.

Keep going

Images are one discipline out of nine. The launch checklist pulls the rows that block a deploy from every discipline into one page, and the performance checklist is where the LCP, resource-hint and caching rules that images depend on live in full. The HTML checklist covers the document these images sit in.

Most of the pipeline rules above are free on a static host: the static site stack leans on Astro's image component, which emits the renditions and the width and height at build time, and the hosting guide covers the platforms that put a CDN in front of the site - and flags platform-specific image optimization as the lock-in it is. If editors upload the images, choose from the CMS guide with the asset pipeline in mind - a system whose asset API returns a transformation URL saves the whole build step.