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
| Param | Values | Description |
|---|---|---|
w / h | 1–4000 | Target width / height in pixels |
fit | cover, contain, scale-down, crop, pad | How the image fills the target box |
q | 1–100 (default 85) | Compression quality — 75–85 is the sweet spot for photos |
f | auto, webp, avif, jpeg, png, gif | Output format — auto negotiates the best the browser supports |
gravity | face, center, auto, left, right, top, bottom, entropy | Focal point when cropping — face keeps heads in avatars |
dpr | 1–3 | Device pixel ratio — serve sharper images to retina screens |
blur / sharpen | 0–250 / 0–10 | Blur or sharpen the image |
rotate / flip | 0|90|180|270; h|v|hv | Rotation and mirroring |
bg | color | Background 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=""plusaria-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
| Content | Recommended |
|---|---|
| Photographs, hero images | q=80 (up to 85 for large full-bleed heroes) |
| Card thumbnails, grid images | q=75 |
| Avatars, small UI images | q=75 with gravity=face for crops |
| Screenshots with text | q=85–90 — compression artifacts blur text first |
| Icons, line art | Keep 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=autoon every image URL — no<picture>format switching neededwalways;h+fit=coverwhen the box is fixedwidth/heightattributes oraspect-ratioto prevent CLSsrcset+sizesfor viewport-spanning imagesloading="lazy"below the fold;fetchpriority="high"on the LCP imageq=75–85; higher only for text-heavy screenshots- Append transforms to R2 signed URLs with
&