Building Image CDN with Firebase
Tags: firebase, serverless, cloud, cdn
Published: 2019-11-10 23:58:26

---
title: Building Image CDN with Firebase
published: true
description: Build your own image CDN with on-the-fly resize and webp conversion
tags: firebase, serverless, cloud, cdn
cover_image: https://thepracticaldev.s3.amazonaws.com/i/a1mbejhtf5wq9f2wz6hu.jpg
---

Delivering a perfect image on the web is not that easy of a task as it seems. Long gone the days where you can just put 'image.jpg' in your '/var/www/dist' and call it a day. You want your website to load images quickly, in the right format and with a perfect size based on the user's device. And you want that without blowing up your cloud storage bills and ideally without tedious work of resizing and tweaking your images manually.

Luckily for us Image CDNs exist. These are specialized types of Content Delivery Networks (CDN) that can take a source image, perform the conversions and transformations on the fly and cache the result to be delivered globally in a matter of milliseconds. Some examples are [Cloudinary](https://cloudinary.com/) and [imgix](https://www.imgix.com/).

In this post we will build our own image CDN - efficient, fast, customizable, living on our own domain, and free using Firebase.

## What we are going to build
In a nutshell we want to:
1. Upload an image `image.jpg` to a storage bucket.
2. Use a specially formatted URL to retrieve that image in the right size, e.g `/cdn/image/width=100,height=50/image.jpg`.
3. That URL should work fast.
4. We want to get the image in the [webp](https://developers.google.com/speed/webp) format if our browser supports that.

## Overall architecture
![Firebase Image CDN Architecture Diagram](https://thepracticaldev.s3.amazonaws.com/i/fqph3oir95hu4n5wk8o4.png)
<figcaption>Btw, this image is served from Cloudinary Image CDN</figcaption>

Some important details about the proposed design:
### Firebase Hosting
The Firebase Hosting part is where the automatic caching magic happens. By design Hosting caches all the responses from Functions that have `Cache-control` HTTP header set to `public` with an appropriate expiration time. That means that subsequent requests to the same image size, even from different browsers, will be served super fast and without firing another Function execution which saves us money.

> Note that Firebase Hosting rewrite rules allow us to co-host our image CDN on the same domain as our main website.

### Firebase Function
The Firebase Function contains the image processing logic. It is also responsible for setting the appropriate response HTTP headers for Firebase Hosting to cache processed images correctly.

### Firebase Storage
The main storage for original images is Firebase Storage and we identify images by their key in the bucket. Using Firebase Storage is good for the speed of access and works on the free tier since we aren't making outbound requests from our functions.

### Browser
Modern browsers that support webp format send a special `Accept` HTTP header with a value `image/webp` (at least Firefox and Chrome do that). We will use that to automatically detect the support and convert the image.

> Note that since the desired image response depends on the HTTP header we also need to include it in the Function's response `Vary` header. This ensures that Firebase Hosting cache won't result in serving webp images to the browsers that don't support it.

## The image transformation function
For image processing we are going to use [sharp](https://github.com/lovell/sharp) JavaScript library. This is super handy for us as we don't need to spawn any additional processes. Also sharp is faster than ImageMagic, so win-win.

You can find the full source code [here](https://github.com/dbanisimov/firebase-image-cdn). But without a single listing that wouldn't be a serious DEV post, right? So here is the snippet of the function implementation:

```js
// Full code is here: https://github.com/dbanisimov/firebase-image-cdn

// Run the image transformation on Http requests.
// To modify memory and CPU allowance use .runWith({...}) method
export const imageTransform = functions.https.onRequest((request, response) => {
  let sourceUrl;
  let options;
  try {
    const [optionsStr, sourceUrlStr] = tokenizeUrl(request.url);
    sourceUrl = new URL(sourceUrlStr);
    options = parseOptions(optionsStr);
  } catch (error) {
    response.status(400).send();
    return;
  }

  // Modern browsers that support WebP format will send an appropriate Accept header
  const acceptHeader = request.header('Accept');
  const webpAccepted =
    !!acceptHeader && acceptHeader.indexOf('image/webp') !== -1;

  // If one of the dimensions is undefined the automatic sizing
  // preserving the aspect ratio will be applied
  const transform = sharp()
    .resize(
      options.width ? Number(options.width) : undefined,
      options.height ? Number(options.height) : undefined,
      {
        fit: 'cover'
      }
    )
    .webp({ force: webpAccepted, lossless: !!options.lossless });

  // Set cache control headers. This lets Firebase Hosting CDN to cache
  // the converted image and serve it from cache on subsequent requests.
  // We need to Vary on Accept header to correctly handle WebP support detection.
  const responsePipe = response
    .set('Cache-Control', `public, max-age=${cacheMaxAge}`)
    .set('Vary', 'Accept');

  // The built-in node https works here
  https.get(sourceUrl, res => res.pipe(transform).pipe(responsePipe));
});
```

And also `firebase.json` rewrites section, which transparently forwards requests to the Function:
```
{
...
  "rewrites": [
    {
      "source": "/cdn/image/**",
      "function": "imageTransform"
    },
    {
      "source": "**",
      "destination": "/index.html"
    }
  ]
...
}
```

## The end result
You can see the live demo here: https://fir-image-cdn.web.app/.

The cat's photo is resized on-the-fly and cached, you can check that by opening it again in a private browsing mode - the result will be noticeably fast. Another way to verify that we are seeing content served by the CDN cache is to check for the `x-cache: HIT` header in the response.

## How fast is it?
Cached responses - low **10ms** for small images which should stay pretty consistent globally.

Cache miss - **500ms+** for small images, up to **1s** for medium images and *seconds (!)* for large images. Whom to blame here is an open question - quick experiment with changing the Firebase Function type to a faster one hasn't shown noticeable difference, so it may not be due to the compute power, but rather Functions outbound networking or CDN cache fill performance.

## Caveats
**Cold starts**. The first Function execution takes significantly longer than subsequent executions, which may affect the user's experience. One way to avoid that is to use [Cloud Run](https://cloud.google.com/run/) containers, which allow concurrent requests to the same instance. The great thing is that Cloud Run is supported by Firebase Hosting as the request forwarding destination.

**Low cache hit ratio**. In our approach we vary the cached response based on the `Accept` header in order to correctly support webp detection. Every browser may send different value for that header which may result in very low cache hit ratio. We may avoid that by disabling the automatic detection and request webp version explicitly. Modern browsers allow to do that with a combination of `<picture>` and `<source type="image/webp">` HTML elements.

## How much does it cost?
Great news everyone! The combo described above works perfectly under the Firebase free Spark plan. You roughly get 5GB of stored source images and 100K image transformations per month. 

It also seems like the cached responses served by CDN count towards the total Hosting download allowance of 10GB per month.

## Next steps
If you want to take the self-hosted Image CDN idea to the next level then take a look at [Thumbor](https://thumbor.readthedocs.io/en/latest/index.html). It has a ton of features and could be easily run inside of a Cloud Run container.

You may get better overall performance (and cost of operations) by manually combining Cloud CDN, Load Balancer, multi-regional Cloud Storage and Cloud Functions deployed in multiple regions. But should you?

## What else to read
* [Optimizing images for the web - an in-depth guide](https://dev.to/prototyp/optimizing-images-for-the-web-an-in-depth-guide-4j7d)
* [Use image CDNs to optimize images](https://web.dev/image-cdns/)
* [Responsive images](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)

<hr />

Stay sharp!