Making assets fly

and images a breeze 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

HTML Responsive Images

<img
     src="/wp-content/uploads/our-image.jpg"
     srcset="
             /wp-content/uploads/our-image-300x200.jpg 300w,
             /wp-content/uploads/our-image-768x512.jpg 768w,
             /wp-content/uploads/our-image-1024x683.jpg 1024w,
             /wp-content/uploads/our-image-1536x1024.jpg 1536w
     "
     sizes="(min-width: 800px) 768px, 100vw"
     ...
 >

Optimisation and Image Format

Size
Source JPEG 300kB
Optimised JPEG 60kB
WebP 🙂 36kB
AVIF * 😀 28kB
JPEG XL * 😍 24kB
Size Chrome Firefox Safari
Source JPEG 300kB yes yes yes
Optimised JPEG 60kB yes yes yes
WebP 🙂 36kB yes yes recent
AVIF * 😀 28kB yes recent no
JPEG XL * 😍 24kB no no no
Size
Source JPEG 300kB
Optimised JPEG 60kB
WebP 🙂 36kB
AVIF * 😀 28kB
JPEG XL * 😍 24kB

Optimisation and Image Format

Hi DPI displays

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

iPhone 12 Pro (right) screen:

  • Screen width: 1170 pixels
  • 'Viewport' width: 390 pixels
  • Device Pixel Ratio of 3

The browser could decide to load an image with resolution 3x larger than it's displayed - 9x increase in file size!!!

We can mitigate increase in file size by increasing the image compression without visible effect.

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
<img
    loading="lazy"
    decoding="async"
 >
<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>

That's a lot going on!

Image processing wishlist

  • Convert to several different formats, update to support new ones like AVIF and JPEG XL for browsers that support them
  • Resize images to many different sizes for responsive devices
  • Handle differing quality levels depending on density
  • Bonus: Perform quality analysis comparing image sizes between different formats
  • Optimise images and compress where possible

To display a few images on a webpage!

Common Image systems

Manual

Custom code and process

Package or component in CMS, template or build tooling

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

Provided or custom tooling

  • May not prioritise compression, resizing or may not convert formats
  • Can be expensive and time-consuming for developers to maintain
  • May use large amounts of resources manipulating images on upload/build, storing every image

HTTP Archive data

Source and image credit:

HTTP Archive Web Almanac, 2021 - Media Chapter

Image issues in the wild

  • Fresh redesign and rebuild
  • Client-managed content
  • Jamstack build tool with headless WordPress CMS
  • Very visual featuring lots of images
  • Was in a reasonably rural area so we wanted to ensure performance was very good
  • 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 image process

  • 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

Results

  • Created additional costs of servers, services and storage
  • Created technical debt
  • Increased time for site updates to go live from ~30 seconds to almost 10 minutes
  • At first it was okay, improved performance slightly

Image
CDNs to
the rescue

What is an Image CDN?

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

and many more

Web Server

HTML

CSS

JS

Images

Images

User's browser

Optimised

Source

Web Server

User's browser

HTML

CSS

JS

Images

Images

Web Server

Optimised

Source

Web Server

User's browser

HTML

CSS

JS

Images

Images

Media Storage

How do we use them?

https://example.imagecdn.com

Start with our image
CDN domain

https://example.imagecdn.com

/https://oursite.com/src/assets

/example.jpg

Add the URL to our
source image

https://example.imagecdn.com

/example.jpg

Many CDN providers allow
you to make presets/aliases

https://example.imagecdn.com

/example.jpg

?w=600&h=400

Add parameters to

'transform' the image

https://example.imagecdn.com/fetch

/example.jpg

/w_600,h_400

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

  • Handled separately from your main site or 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,
    e_colorize:30,co_rgb:dd14d1/...

Wide banner anchored to top, coloured pink

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>

Cloudinary WordPress plugin

CMS' plugins or platform integrations

Imagekit.io integrations

<img src="{{ image | img_url: '800x300', crop: 'center' }}" ...>

Shopify Image CDN

{% 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

Templating language shortcodes

React Component

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

PHP Function

<?php
echo image([
  'image'   => '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'
]);

accud.io/imagecdn-php

Image CDNs make images
easier—everywhere!

Moving to using an Image CDN

What we did

  • Used the CloudImage Image CDN
  • Set up a custom code solution using a Liquid shortcode like in previous example
  • Removed custom toolchain

Lighthouse performance score improved by ~19 points

Increased speed of development

Reduced site build time by 80%

Chrome Image bytes
decreased by 27%

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 or faffing with plugins
  • Can be offset by reduced server load, storage requirements or build infrastructure
  • Disable CDN for local development
     
  • Offload images to cloud storage


     
  • Push images to your site/secret dev URL when needed

Local dev

For very high performing sites different origin has an impact

Solutions for nginx and Netlify

location /cdn/ {
  proxy_pass https://example.imagecdn.com/;
}
[[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 resources required for server/build

Try an Image CDN in your next project!

Thank You!

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

On twitter and most social media    @accudio

My website and blog posts    alistairshepherd.uk

Made with Slides.com