Image Optimization

Every image Lumen serves — static assets and R2 files alike — goes through Cloudflare Images at the edge: resize, crop, format negotiation, and compression, cached for a year. Never ship the original file.

The basics

Any image URL accepts query parameters that transform the image on the fly. The transformed variant is generated once at the edge and cached for a year — subsequent requests are served from the nearest Cloudflare location:

/images/hero.jpg?w=1600&h=600&fit=cover&f=auto&q=80

Because R2 signed URLs already contain ?exp=...&sig=..., append transforms with & instead of ?:

<img src="{{ data.cover.url }}&w=800&h=600&fit=cover&f=auto" />

Rule of thumb: for every <img> you output — set f=auto, size to the display box, set width/height attributes, and choose a loading strategy. Details below.

Parameter reference

ParamValuesDescription
w / h1–4000Target width / height in pixels
fitcover, contain, scale-down, crop, padHow the image fills the target box
q1–100 (default 85)Compression quality — 75–85 is the sweet spot for photos
fauto, webp, avif, jpeg, png, gifOutput format — auto negotiates the best the browser supports
gravityface, center, auto, left, right, top, bottom, entropyFocal point when cropping — face keeps heads in avatars
dpr1–3Device pixel ratio — serve sharper images to retina screens
blur / sharpen0–250 / 0–10Blur or sharpen the image
rotate / flip0|90|180|270; h|v|hvRotation and mirroring
bgcolorBackground color for fit=pad or flattening transparency

Format negotiation

Always set f=auto. The edge serves AVIF to browsers that support it, WebP to the rest, and JPEG/PNG only as a last resort — typically 30–50% smaller files with no visible quality loss. And because negotiation happens per request, you do not need <picture> elements or multiple format sources — one URL covers every browser:

<!-- one URL, every browser gets its best format -->
<img src="/images/photo.jpg?w=800&f=auto&q=80" ... />

Sizing to the display box

Match the transform to the rendered size — a 400×300 card thumbnail should not download a 4000px original:

<!-- fixed box: w + h + fit=cover -->
<img src="/images/post.jpg?w=400&h=300&fit=cover&f=auto&q=75"
     width="400" height="300" loading="lazy" alt="..." />

<!-- avatar: square crop focused on the face -->
<img src="/images/me.png?w=96&h=96&fit=cover&gravity=face&f=auto"
     width="96" height="96" loading="lazy" alt="..." />

Setting width and height attributes (or a CSS aspect-ratio) reserves the space before the image loads — that's what prevents cumulative layout shift (CLS).

Responsive images

When an image spans a range of viewport widths, emit a srcset of pre-sized variants plus a sizes attribute so each device downloads only what it needs:

<img src="/images/hero.jpg?w=1200&fit=cover&f=auto&q=80"
     srcset="/images/hero.jpg?w=480&fit=cover&f=auto&q=75 480w,
             /images/hero.jpg?w=800&fit=cover&f=auto&q=75 800w,
             /images/hero.jpg?w=1200&fit=cover&f=auto&q=80 1200w"
     sizes="(max-width: 600px) 100vw, 1200px"
     width="1200" height="500" alt="..." />

For high-density screens, add dpr=2 variants to the srcset, or serve one box-sized dpr=2 image when the display size is fixed.

Loading strategy

  • LCP image (usually the hero): fetchpriority="high", no lazy loading, and keep it out of any client-side fade-in that delays paint.
  • Everything below the fold: loading="lazy" — the browser fetches only as the image approaches the viewport.
  • Decorative images: alt="" plus aria-hidden="true" so screen readers skip them.
<!-- hero: LCP candidate -->
<img src="/images/hero.jpg?w=1600&h=600&fit=cover&f=auto&q=80"
     width="1600" height="600" fetchpriority="high" alt="..." />

Quality guidance

ContentRecommended
Photographs, hero imagesq=80 (up to 85 for large full-bleed heroes)
Card thumbnails, grid imagesq=75
Avatars, small UI imagesq=75 with gravity=face for crops
Screenshots with textq=85–90 — compression artifacts blur text first
Icons, line artKeep PNG, or f=auto and let the edge decide

Caching

Transformed variants are cached at the edge for a year. Changing a parameter (bumping w, lowering q) produces a new cache entry immediately — no invalidation to manage. To "update" an image, upload a new object or add a cache-busting query flag of your own.

Checklist

  • f=auto on every image URL — no <picture> format switching needed
  • w always; h + fit=cover when the box is fixed
  • width/height attributes or aspect-ratio to prevent CLS
  • srcset + sizes for viewport-spanning images
  • loading="lazy" below the fold; fetchpriority="high" on the LCP image
  • q=75–85; higher only for text-heavy screenshots
  • Append transforms to R2 signed URLs with &