There's not a lot of CDNs (that we can use) that are designed to handle being put it front of a web application.
We've recently deployed our own edge network around the globe for performance and redundancy. The edge nodes handle TLS termination, keep upstream connections open, and provide a basic caching layer which significantly improved our performance.
However, there are limitations. If we want to handle functionality like automatically serving WebP, this introduces a lot more complexity into our edge stack.
The simplest way to improve performance on our blog was to ensure that images are served by a CDN that is capable of this stuff. We need to change the image URLs returned by Ghost so they point to the CDN.
Ghost does not provide a feature to let you set a CDN for images, which seems rather odd to me, but I suspect it has to do with their hosted offering.
Our starting point is a PageSpeed score of 87 on mobile.
Most of our penalties come from Speed Index and LCP, as the images aren't really optimised and have to be sent from our origin in Europe to the US.
We can leverage the basic caching ability of our edge network to cache the images at our edge.
After creating a rule that caches the images on the edge, our image load timings went from around 1000ms to about 250ms, which is a significant improvement for not that much work! That is, if you don't include the work to set up your own edge network.. which took me about a week's worth of work.
Our resulting Pagespeed score is now 90 on mobile.
However, our Speed Index could still be improved by serving modern image formats with better compression.
Using a CDN to serve optimised images
Rather than complicate our Ghost stack too much, I went for the option of using a CDN that handles image optimisation for us.
Once I verified that the CDN'd endpoint worked correctly, the problem became making Ghost use the domain for serving images.
My first few attempts at this had me going down a rabbit hole with lua-nginx-module, but turns out openresty has
subs_filter built in, which provides more features compared to the stock
After implementing this, our mobile Pagespeed went to 96.
By transmitting less data, our image load time has dropped from 250ms to 100ms!
Bonus: Native lazy loading
subs_filter <img "<img loading=\"lazy\"" g;
However, this does not appear to make a difference to the Pagespeed score.
I hope this is useful!