Optimising image delivery at the edge

In my recent post about Street Photography in South Korea, I embedded many high-res pictures. The total size of the page is about 40 MB, which is a bit much to load right away.

To reduce the network payload and performance during the initial page load, I thought about adding image lazy-loading. Although I haven’t been a fan of the custom javascript-dependant ways of doing that, I’m pleased to see that it is now browser native.

I did some research, but sadly Ghost does not support (yet?) lazy-loading.

Info

Ghost now has native lazy loading since version 4. (source)

Sadly, even though I have a custom Ghost theme, I can’t modify the actual post content because the theme calls a helper for that part:

{{!< default}}

<div class="content-area">
  <main class="site-main">
    {{#post}}
      {{> content}} # can't access what's in this :(
    {{/post}}
  </main>
</div>

I stumbled upon this post by James Ross explaining how they extended Ghost functionality by using Cloudflare Workers. I took some of the code and kept the part that interested me: lazy-loading.

Since my blog is proxied by Cloudflare, I can use their Workers service to transform the resources being sent to the clients, right at the edge. In the current case, I want to manipulate the DOM with HTMLRewriter.

Below is the code being applied to every response from stanislas.blog:

class ElementHandler {
  element(element) {
    if (!element.getAttribute("loading")) {
      element.setAttribute("loading", "lazy");
    }
  }
}

async function handleRequest(req) {
  const res = await fetch(req);

  return new HTMLRewriter().on("img", new ElementHandler()).transform(res);
}

addEventListener("fetch", (event) => {
  event.respondWith(handleRequest(event.request));
});

It’s pretty self-explanatory. Besides the light boilerplate for the Workers service, it gets every img element and adds loading="lazy" if it’s not there already.

If we test that on the post I mentioned earlier:

No lazy-loading
Lazy-loaded images

The initial page load is now much lighter and additional images will be loaded when they approach the viewport. Images that are in the viewport during the page load (like the header) will be loaded without delay. Test it for yourself!

More on Cloudflare Workers

Since this is a very basic Worker, I did everything I needed to right from the Cloudflare dashboard.

worker code

Deploying a worker is very easy:

deploy worker

I basically apply it to every request to stanislas.blog. I couldn’t find a route path that ignored non-post paths like static assets or the API, so this isn’t very efficient.

However, it doesn’t matter for two reasons. First, I can allow myself to do more worker requests than necessary, because on the free plan I have 100k requests per day for free, which is a lot. I chose the “Fail open” failure mode for this worker: over 100k requests, it will simply stop lazy loading images, which is really not a big deal. But of course, this wouldn’t be a sane approach for a more “important” Worker.

worker metrics total requests

The other reason is that this Worker introduces very little overhead. By little, I mean microseconds of delay.

worker metrics CPU time

This is insanely fast. And most of my pages are cached by Cloudflare anyway.

worker metrics cache
worker metrics status code

All in all, this was a very cool way of getting to know Cloudflare Workers, and all I can say is that I’m very impressed by the product.