Adding native image lazy-loading to Ghost with a Cloudflare Worker
3 min read

Adding native image lazy-loading to Ghost with a Cloudflare Worker

Optimising image delivery at the edge
Adding native image lazy-loading to Ghost with a Cloudflare Worker

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.

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.

Deploying a worker is very easy:

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.

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

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

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.