Olympus Docs
CookbookUI & content

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=cover

URL parameters control resize. Cost: ~$10/mo + bandwidth.

Cloudinary

https://res.cloudinary.com/.../image/upload/w_128,h_128,c_fill/v123/avatars/abc.webp

Similar. Free tier generous.

Bunny.net Optimizer

https://your.b-cdn.net/avatars/abc.webp?width=128&height=128

Cheap.

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 item

srcset 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=face

Centers 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 AVIF

Less markup, same result.

Quality

Adjust quality for size vs file:

?q=80

80% 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.

On this page