CDN-based image resizing
Serve right-sized avatars without storing many variants
When you have user avatars, you typically need them at 32px, 64px, 128px, 256px, 512px. Storing all variants = 5x storage. Better: have CDN resize on-the-fly.
Options
Cloudflare Images
https://imagedelivery.net/<account-hash>/<image-id>/<variant>Per-image, variants defined globally. Cost: $0.10 per 100k transforms.
Imgix
https://your-cdn.imgix.net/avatars/abc.webp?w=128&h=128&fit=coverURL parameters control resize. Cost: ~$10/mo + bandwidth.
Cloudinary
https://res.cloudinary.com/.../image/upload/w_128,h_128,c_fill/v123/avatars/abc.webpSimilar. Free tier generous.
Bunny.net Optimizer
https://your.b-cdn.net/avatars/abc.webp?width=128&height=128Cheap.
Self-hosted with Caddy + caddy-images
img.your-domain.com {
reverse_proxy {
to images:3000
}
}Service resizes via Sharp; caches. More control, more ops.
Implementation
function Avatar({ user, size = 64 }) {
const url = `${IMG_CDN}/avatars/${user.id}.webp?w=${size}&h=${size}&fit=cover`;
return <img src={url} loading="lazy" alt="" />;
}// Different size for hero vs list
<Avatar user={u} size={256} /> // profile page
<Avatar user={u} size={32} /> // list itemsrcset for high-DPI
function Avatar({ user, size }) {
return (
<img
src={`${IMG_CDN}/avatars/${user.id}?w=${size}&h=${size}`}
srcSet={`
${IMG_CDN}/avatars/${user.id}?w=${size} 1x,
${IMG_CDN}/avatars/${user.id}?w=${size * 2} 2x,
${IMG_CDN}/avatars/${user.id}?w=${size * 3} 3x
`}
alt=""
/>
);
}Retina displays get higher-res; standard get standard. Bandwidth-aware.
Cache
CDN caches transforms. First request: slow (transform happens). Subsequent: instant.
For predictable use cases (always 64px on list), CDN cache warm. For rare sizes (300x300 on a one-off page), no cache.
Smart cropping
Some CDNs do face detection:
?w=128&h=128&fit=faceCenters on face, not random.
Cloudinary, Imgix support. Saves UI from awkward crops.
Format conversion
Serve WebP or AVIF if browser supports, fallback to JPEG:
<picture>
<source srcset={`${url}?format=avif`} type="image/avif" />
<source srcset={`${url}?format=webp`} type="image/webp" />
<img src={`${url}?format=jpeg`} alt="" />
</picture>CDN delivers right format. Modern CDNs negotiate via Accept header automatically:
<img src={url} alt="" /> // CDN sees Accept: image/avif, serves AVIFLess markup, same result.
Quality
Adjust quality for size vs file:
?q=8080% quality: virtually indistinguishable from 100%, much smaller.
For avatars (small images), even q=60 looks fine.
Lazy load
For grids of avatars:
<img loading="lazy" src={...} />Browser loads only what's in viewport. Saves bandwidth on long pages.
Placeholder
While loading, show placeholder:
<img
src={fullUrl}
loading="lazy"
style={{ backgroundColor: hashedColor(user.id) }}
/>Or LQIP (Low-Quality Image Placeholder):
<img
src={fullUrl}
data-lqip={`${url}?w=8&h=8`}
/>8x8 blurred preview while real loads.
Privacy
For private avatars (apps where avatars aren't public): signed URLs with TTL.
const url = `${IMG_CDN}/avatars/${id}?w=128&signature=${sign(...)}&expires=${Date.now() + 3600_000}`;URL expires; can't be shared widely.
SVG considerations
Some CDNs don't transform SVG (vector). They'll serve as-is.
For dynamic icons, just include SVG directly in HTML, no CDN transform needed.
Cost
Cloudflare Images: 100k images stored + 1M transforms ~ $5/mo. Imgix: 10k images, 100k transforms ~ $10/mo.
Trivial for typical apps. Scales with usage.
Self-hosted alternative
Sharp via Node:
import sharp from "sharp";
app.get("/img/:id", async (req, res) => {
const original = await s3.getObject(`avatars/${req.params.id}.webp`);
const buffer = await sharp(original.Body)
.resize(parseInt(req.query.w), parseInt(req.query.h))
.toBuffer();
res.set("content-type", "image/webp")
.set("cache-control", "public, max-age=86400")
.send(buffer);
});In front of CDN (Caddy with caching). Cost: just hosting.
For Olympus deployment scale: viable. Save the $5/mo.