| name | image-delivery |
| description | Deliver responsive images via CDN with AVIF/WebP, placeholders, and progressive loading on iOS, Android, Flutter, and React Native. |
Image Delivery
Instructions
Image delivery has two sides: the pipeline that produces the right image at the right size for the right device, and the client that displays it without jank. This skill covers both.
1. CDN-Side Contract
Standard query API (examples: Cloudinary, imgix, Cloudflare Images, Fastly Image Optimizer):
/img/{id}?w=720&h=0&fit=cover&q=80&fm=auto
w / h — target pixels. h=0 preserves aspect.
fit — cover | contain | crop.
q — quality (75–85 for AVIF, 80–85 for WebP).
fm=auto — CDN picks AVIF > WebP > JPEG based on Accept.
Emit headers:
Cache-Control: public, max-age=31536000, immutable
Vary: Accept
2. Pick the Right URL on the Client
Compute pixel size from the view, not the design canvas. Account for device scale.
iOS:
extension UIView {
func scaledPx(_ logical: CGFloat) -> Int { Int(logical * UIScreen.main.scale) }
func imageUrl(_ id: String) -> URL {
let w = scaledPx(bounds.width)
return URL(string: "https://cdn.example.com/img/\(id)?w=\(w)&fm=auto&q=80")!
}
}
Android:
fun ImageView.imageUrl(id: String): String {
val w = (width * resources.displayMetrics.density).toInt()
return "https://cdn.example.com/img/$id?w=$w&fm=auto&q=80"
}
Flutter:
String imageUrl(BuildContext c, String id, double logicalW) {
final w = (logicalW * MediaQuery.of(c).devicePixelRatio).round();
return 'https://cdn.example.com/img/$id?w=$w&fm=auto&q=80';
}
RN:
const px = (logical: number) => Math.round(logical * PixelRatio.get());
const url = (id: string, w: number) => `https://cdn.example.com/img/${id}?w=${px(w)}&fm=auto&q=80`;
3. Placeholders
Options, cheapest first:
- Solid color — extract average color from the image at upload time, send it as
color: "#c0b8a8" alongside the image URL.
- BlurHash / ThumbHash — a 20–30-byte hash that decodes to a tiny blurred preview.
- LQIP (low-quality image placeholder) — 4–8 KB JPEG inlined as base64 or prefetched.
- Skeleton shimmer — animated gradient. Cheap to draw but moves; do not use for above-the-fold hero.
BlurHash in Kotlin (Coil):
imageView.load(url) {
placeholder(BlurHashDrawable(blurHashString, 32, 32))
crossfade(200)
}
SwiftUI + BlurHash:
AsyncImage(url: url) { phase in
switch phase {
case .success(let img): img.resizable().scaledToFill()
default: Color(uiColor: UIImage(blurHash: blurHashString, size: .init(width: 32, height: 32)) ?? .systemGray5.withAlphaComponent(1))
}
}
4. Progressive Loading
Ship two variants in parallel:
- Tiny (w=40) delivered inline or immediately.
- Full-size delivered progressively.
HTML progressive JPEGs still work over HTTP/2. AVIF has progressive tiles; WebP does not — fall back to two-tier.
RN example with Expo Image:
<Image
source={{ uri: fullUrl }}
placeholder={{ blurhash }}
transition={200}
contentFit="cover"
priority={isAboveFold ? 'high' : 'normal'}
/>
5. Prefetch for Next Screen
When the user is on a list and a detail screen is likely, prefetch the hero image.
Coil:
imageLoader.enqueue(ImageRequest.Builder(ctx).data(heroUrl).build())
SDWebImage:
SDWebImagePrefetcher.shared.prefetchURLs([heroURL])
Flutter:
precacheImage(CachedNetworkImageProvider(heroUrl), context);
6. Avoid Layout Shift
Reserve space before the image arrives: set aspectRatio (or explicit width + height) on the container. Without it, content below shifts when the image swaps in, causing cumulative layout shift and input misfires.
SwiftUI:
AsyncImage(url: url).aspectRatio(16/9, contentMode: .fill)
Compose:
AsyncImage(url = url, modifier = Modifier.aspectRatio(16 / 9f))
Flutter:
AspectRatio(aspectRatio: 16 / 9, child: CachedNetworkImage(imageUrl: url))
7. Video Thumbnails
Never decode the full video to get a poster; generate posters at upload time and deliver as regular images. If you must on-device, use AVAssetImageGenerator / MediaMetadataRetriever off the main thread and cache the result.
8. Measure
Track per-image:
- Bytes downloaded.
- Source (memory / disk / network).
- Decode time.
- Time from request → visible pixel.
p95 "first byte → visible pixel" should be under 500 ms on 4G for above-the-fold images.
Checklist