Making assets fly on the Jamstack with Image CDNs

Alistair Shepherd - Front-End Developer

SeriesEight

Images on the web

<img
  src="/our-image.jpg"
  alt="our image"
>

Images change size depending on screen width

Source jpeg - 300kB

Webp - 18kB 🙂

JPEG XL* - 12kB 😍

Optimised jpeg - 30kB 😮

AVIF - 14kB 😀

Hi DPI images

Lazy Loading and Decoding

  • "loading" attribute set to "lazy" or "eager" depending on position on page
  • decoding="async" when images shouldn't delay text rendering
<picture>
    <source 
      media="(-webkit-min-device-pixel-ratio: 1.5)"
      srcset="https://example.com/images/our-image_width1200_quality40.jpg 1200w"
      sizes="...">
    <img
      srcset="https://example.com/images/our-image_width600_quality80.jpg 600w"
      sizes="..."
      alt="">
</picture>
<picture>
  <source
      type="image/avif"
      media="(-webkit-min-device-pixel-ratio: 1.5)"
      srcset="/assets/image/otter-300-q40.avif 300w, /assets/image/otter-450-q40.avif 450w, /assets/image/otter-600-q40.avif 600w,
              /assets/image/otter-800-q40.avif 800w, /assets/image/otter-1000-q40.avif 1000w, /assets/image/otter-1200-q40.avif 1200"
      sizes="(min-width: 87rem) 40rem, (min-width: 768px) calc(50vw - 4rem), calc(100vw - 2rem)">
  <source
      type="image/avif"
      srcset="/assets/image/otter-300-q80.avif 300w, /assets/image/otter-450-q80.avif 450w, /assets/image/otter-600-q80.avif 600w,
              /assets/image/otter-800-q80.avif 800w, /assets/image/otter-1000-q80.avif 1000w, /assets/image/otter-1200-q80.avif 1200"
      sizes="(min-width: 87rem) 40rem, (min-width: 768px) calc(50vw - 4rem), calc(100vw - 2rem)">
  <source
      type="image/webp"
      media="(-webkit-min-device-pixel-ratio: 1.5)"
      srcset="/assets/image/otter-300-q40.webp 300w, /assets/image/otter-450-q40.webp 450w, /assets/image/otter-600-q40.webp 600w,
              /assets/image/otter-800-q40.webp 800w, /assets/image/otter-1000-q40.webp 1000w, /assets/image/otter-1200-q40.webp 1200"
      sizes="(min-width: 87rem) 40rem, (min-width: 768px) calc(50vw - 4rem), calc(100vw - 2rem)">
  <source
      type="image/webp"
      srcset="/assets/image/otter-300-q80.webp 300w, /assets/image/otter-450-q80.webp 450w, /assets/image/otter-600-q80.webp 600w,
              /assets/image/otter-800-q80.webp 800w, /assets/image/otter-1000-q80.webp 1000w, /assets/image/otter-1200-q80.webp 1200"
      sizes="(min-width: 87rem) 40rem, (min-width: 768px) calc(50vw - 4rem), calc(100vw - 2rem)">
  <source
      media="(-webkit-min-device-pixel-ratio: 1.5)"
      srcset="/assets/image/otter-300-q40.jpg 300w, /assets/image/otter-450-q40.jpg 450w, /assets/image/otter-600-q40.jpg 600w,
              /assets/image/otter-800-q40.jpg 800w, /assets/image/otter-1000-q40.jpg 1000w, /assets/image/otter-1200-q40.jpg 1200"
      sizes="(min-width: 87rem) 40rem, (min-width: 768px) calc(50vw - 4rem), calc(100vw - 2rem)">
  <img
      alt="otter standing on a log looking out majestically"
      src="/assets/image/otter-1200-q80.jpg"
      srcset="/assets/image/otter-300-q80.jpg 300w, /assets/image/otter-450-q80.jpg 450w,
              /assets/image/otter-600-q80.jpg 600w, /assets/image/otter-800-q80.jpg 800w,
              /assets/image/otter-1000-q80.jpg 1000w, /assets/image/otter-1200-q80.jpg 1200"
      sizes="(min-width: 87rem) 40rem, (min-width: 768px) calc(50vw - 4rem), calc(100vw - 2rem)"
      width="1200"
      height="784"
      loading="lazy"
      decoding="async"
  >
</picture>

To display a few images on a webpage!

Images on the Jamstack

Manual

Custom code and process

Package or component like
next/image or 11ty-image

  • Jpeg, Webp, AVIF
  • Many image sizes
  • Quality levels for 1x and 2x dpi

Image processing wishlist

  • Convert to several different formats, update to support new ones like AVIF and JPEG XL
  • Resize images to many different sizes
  • Handle differing quality levels depending on density
  • Bonus: Perform quality analysis comparing image sizes between different formats
> eleventy-base-blog@5.0.2 build
> eleventy

[...]

Copied 957 files / Wrote 19 files in 188.07 seconds (9898.4ms each, v0.12.1)

3 minutes, 8 seconds!

> eleventy-base-blog@5.0.2 build
> eleventy

[...]

Copied 2 files / Wrote 19 files in 0.59 seconds (31.1ms each, v0.12.1)

Jamstack image issues in the wild

  • New site design and build
  • Client-managed content
  • Jamstack using 11ty and headless WordPress
  • Around 20 artists
  • Up to 40-50 pieces each
  • Hundreds of studio-quality images

"The art on the new website has to be as high quality as possible"

I built my own process using Sharp

  • Converted to Webp, was easy to add AVIF in future
  • Resize images to many different sizes
  • Handle differing quality levels depending on density
  • Bonus: Perform quality analysis comparing image sizes between different formats

Builds went from ~30 seconds to almost
     12 minutes

Image
CDNs to
the rescue

What is an Image CDN?

  • Content Delivery Network (CDN) specialising in image processing
  • Sits between your server and the user 'transforming' images on the fly by customising URLs
  • Third-party services selling solutions to make dev experience of working with images easier and improving performance

and many more

User's browser

Server

HTML

CSS

JS

Images

Images

User's browser

Server

HTML

CSS

JS

Images

Image CDN

Optimised

Images

Source

Server

User's browser

Server

HTML

CSS

JS

Images

Image CDN

Optimised

Images

Source

Media Storage

How do we use them?

https://example.imagecdn.com

Start with our image
CDN domain

https://example.imagecdn.com

/https://oursite.com

/images/example.jpg

Add the URL to our
source image

https://example.imagecdn.com

/images/example.jpg

Many CDN providers allow
you to make presets/aliases

https://example.imagecdn.com

/images/example.jpg

?w=600&h=400

Add parameters to

'transform' the image

https://example.imagecdn.com/fetch

/images/example.jpg

/w_600,h_400

Different CDNs have different formats,
but work on the same concepts

  • Handled outwith site build
  • Managed by image experts
  • Automatic format detection
.../w_300,h_600,c_thumb,g_auto/...

Tall banner with automatic gravity

.../w_1000,h_400,c_fill,g_north/...

Wide banner anchored to top

Combining images and text

Circles and rounded corners

Implementing
Image CDNs

<picture>
  <source
      media="(-webkit-min-device-pixel-ratio: 1.5)"
      srcset="
        https://example.imagecdn.com/images/w_300,q_40/otter.jpg 300w,
        https://example.imagecdn.com/images/w_450,q_40/otter.jpg 450w,
        https://example.imagecdn.com/images/w_600,q_40/otter.jpg 600w,
        https://example.imagecdn.com/images/w_800,q_40/otter.jpg 800w,
        https://example.imagecdn.com/images/w_1000,q_40/otter.jpg 1000w,
        https://example.imagecdn.com/images/w_1200,q_40/otter.jpg 1200w"
      sizes="(min-width: 87rem) 40rem, (min-width: 768px) calc(50vw - 4rem), calc(100vw - 2rem)">
  <img
    alt="otter standing on a log looking out majestically"
    src="https://example.imagecdn.com/images/w_800,q_80/otter.jpg"
    srcset="
      https://example.imagecdn.com/images/w_300,q_80/otter.jpg 300w,
      https://example.imagecdn.com/images/w_450,q_80/otter.jpg 450w,
      https://example.imagecdn.com/images/w_600,q_80/otter.jpg 600w,
      https://example.imagecdn.com/images/w_800,q_80/otter.jpg 800w,
      https://example.imagecdn.com/images/w_1000,q_80/otter.jpg 1000w,
      https://example.imagecdn.com/images/w_1200,q_80/otter.jpg 1200w"
    sizes="(min-width: 87rem) 40rem, (min-width: 768px) calc(50vw - 4rem), calc(100vw - 2rem)"
    width="1200"
    height="784"
    loading="lazy"
    decoding="async"
  >
</picture>
{% image {
  name: 'otter.jpg'
  alt: 'an otter standing on a log looking majestic'
  srcset: [300, 450, 600, 800, 1000, 1200]
  sizes: '100vw'
  width: 1200
  height: 800
  loading: 'lazy'
  class: 'image-class mb-xl'
} %}

accud.io/imagecdn-11ty

import Image from 'components/Image'

export default function () {
  return (
    <Image
      src="otter.jpg"
      alt="an otter standing on a log looking majestic"
      srcset={[300, 450, 600, 800, 1000, 1200]}
      sizes="100vw"
      width="1200"
      height="800"
      loading="lazy"
      className="image-class mb-xl"
    />
  )
}

accud.io/imagecdn-react

Image CDNs makes working with images as easy as formatting a URL

Not just Jamstack!

Moving to using an Image CDN

Build times
reduced by 80%

LCP improved by 19%

Chrome Image bytes
decreased by 27%

Multiple image sources combined into a single endpoint

cms.1896gallery.com/wp-content/uploads/2019/08/artwork-img.jpg
1896gallery.com/assets/internal-graphic.jpg

Image
CDN

1896gallery.imagecdn.com/wp/artwork-img.jpg

1896gallery.imagecdn.com/assets/internal-graphic.jpg

No free lunch!

Cost

  • As an external service, there is a cost implication
  • Many have a generous free tier, small site might not cost anything
  • Larger sites will cost, pricing model depends on the provider
  • Unless you have hundreds of thousands of images, likely cheaper than developers maintaining image processing
  • Can be offset by reduced build minutes if paying for build infrastructure, eg Netlify
  • Disable CDN for local dev
  • Push images to secret public dev URL

For very high performing sites different origin has an impact

Easy solution with Netlify

[[redirects]]
  from = '/cdn/:image'
  to = 'https://example.imagecdn.com/:image'
  status = 200

The scores are in...

Improve the performance of images on your websites

Make working with images easier during development

Reduce Jamstack build times

Try an Image CDN in your next project!

Thank You!

These slides, my sources and helpful links on image CDNs available at    accud.io/jamstack-imagecdns

On twitter and most social media    @accudio

My website and blog posts    alistairshepherd.uk

Making assets fly on the Jamstack with Image CDNs

By Alistair Shepherd

Making assets fly on the Jamstack with Image CDNs

  • 451