Tweaking Ghost for performance

Over engineered Mar 12, 2021

I host my blog using Ghost, which is a lean blogging platform. Offering a friendlier user experience than static site generators such a Hugo and Jekyll without becoming as bloated as Wordpress.

Being the lean platform it is, Ghost is rather quick without any additional tweaks, so why spend any time at all extracting every last bit of performance? Especially when most users will never even notice it? Because I think it is fun and educational as it touches on basically all aspects of hosting a website.

Prerequisites

My setup is fairly straightforward, I am running Ghost using the official Docker container using Docker Compose. Traefik is used as the reverse proxy and SSL terminator.

Baseline

Before we can begin tweaking, we should first take a baseline so we can later see the fruits of our labor. As you can see plain Ghost is already plenty fast by itself.

Screenshot of GTMetrix results (77 / 100)
GTMetrix results
Pingdom results (89 / 100)
Pingdom results

For the curious reader, the Pingdom results are still available.

PageSpeed Insights results on mobile (85 / 100)
PageSpeed Insights results on mobile
PageSpeed Insights results on desktop

Gzip

One of the things Pingdom screamed at the loudest was the fact Gzip is not enabled. Unfortunately Ghost does not support Gzip natively, luckily enabling Gzip in Nginx is an easy feat.

niek-tech-nginx:
  image: nginx:1.19
  <<: *restart-policy
  volumes:
    - /opt/burrow/niek.tech/nginx.conf:/etc/nginx/conf.d/default.conf:ro
  networks:
    - web
  labels:
    # Traefik
    - traefik.enable=true
    - traefik.http.routers.niektech.rule=Host(`niek.tech`)
    - traefik.http.routers.niektech.tls=true
    - traefik.http.routers.niektech.tls.certresolver=le
    - traefik.http.services.niektech.loadbalancer.server.port=80
    # Watchtower
    - com.centurylinklabs.watchtower.enable=true
Relevant section of docker-compose.yaml

This snippet is used to run an Nginx container and making sure Traefik knows about the new service. Make sure you remove the Traefik labels from the Ghost container.

server {
    listen       80;
    server_name  localhost;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header HOST $http_host;
        proxy_set_header X-NginX-Proxy true;

        proxy_pass http://niek-tech:2368;
        proxy_redirect off;
    }
}
Basic nginx.conf file

A quick docker-compose up and the blog should work exactly the way it did 10 minutes ago, but now requests are proxied via Nginx.

Enabling Gzip

Now that Nginx is working as expected we can tune the Gzip parameters. I find the official documentation is an excelent starting point on how to accomplish this. Bottomline, this is the new config file.

server {
    listen       80;
    server_name  localhost;

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header HOST $http_host;
        proxy_set_header X-NginX-Proxy true;

        proxy_pass http://niek-tech:2368;
        proxy_redirect off;
    }
}

One more docker-compose up and Pingdom is happy.

Using a CDN

Using a CDN can help offloading some of the work load to a third party as well as speeding the loading of assets files up by moving them physically closer to the user.

For the CDN I like using Bunny CDN as it is cheap, fast and easy to use. It also has cutesy loading animations with is a small plus as well.

In Bunny creating a "pull zone" is straight forward. This effectively checks if Bunny has the requested path in its cache already, if not it will be requested from the origin URL. This makes the setup simple to configure as all you need to change is the asset domain.

Screenshot of Bunny pull zone configuration

Optionally an custom domain can be used by creating a subdomain with a CNAME record pointing to the original Bunny URL. In this case cdn.niek.tech points to niek-tech.b-cdn.net. SSL is handled by Bunny using a certificate provided by Let's Encrypt.

Screenshot of Bunny's linked hostnames to configure a custom CDN domain

Once the pull zone is configured and working, it needs to be implemented on asset URLs. This could be done by forking the theme and editing the URLs in the relevant templates and if I were using a custom theme that would be the only option. However, if at all possible I would like to keep using  the vanilla Ghost container without any modifications.

Editing responses with Nginx

The real reason I choose to use Nginx, is that it supports sub_filters. This allows to replace strings with another string. Effectively this allows me to replace all occurances of /assets with https://cdn.niek.tech/assets.

I accomplished this by adding the snippet below to the location block.

sub_filter_once off;
sub_filter_types text/html;

# Assets
sub_filter "https://cdn.niek.tech/assets" "https://cdn.niek.tech/assets";
sub_filter "src=\"/assets" "src=\"https://cdn.niek.tech/assets";
sub_filter "href=\"/assets" "href=\"https://cdn.niek.tech/assets";

# Media
sub_filter "https://cdn.niek.tech/content" "https://cdn.niek.tech/content";
sub_filter "src=\"/content" "src=\"https://cdn.niek.tech/content";
Snippet of Nginx sub_filters to replace the CDN domain

Preloading

This can be optimized slightly further by ensuring the CDN domain is preloaded. By adding the relevant tags to the HTML head. Luckily, Ghost provides an option to edit the head directly from the CMS under the section Code Injection.

<!-- Preconnect to CDN --> 
<link rel="preconnect" href="https://cdn.niek.tech">
<link rel="dns-prefetch" href="https://cdn.niek.tech">
<!-- End CDN --> 
Snippet to suggest to the browser to pre-load the CDN domain

Conclusion

First, the scores after the tweaks.

GTMetrix results after tweaking (99 / 100)
GTMetrix results after Tweaking
Pingdom results after Tweaking (85 / 100)
Pingdom results after Tweaking

As before the detailed result are available.

PageSpeed Insights results on mobile after tweaking (93 / 100)
PageSpeed Insights results on mobile after tweaking
PageSpeed Insights results on desktop after tweaking (99 / 100)
PageSpeed Insights results on desktop after tweaking

As you can see the Pingdom score is actually lower than it was before the tweaks were applied. This is because Bunny uses Brotli compression where possible, however at the time of writing Pingdom does not detect this and keeps suggesting to enable Gzip compression. If supported by the browser Brotli compression is the preferred method however.

Screenshot of Pingdom suggesting to enable Gzip
Screenshot of Pingdom suggesting to enable Gzip

As long as this issue stands, the Pingdom score will actually be lower once Bunny is implemented. This shows the biggest downside of chasing perfect scores on website analysis tools, they often fail to see the big picture and in some cases disregard how an actual user sees and uses the site.

Implementing the suggestions made by these sites can be a fun and worthwhile activity, but should be taken with a grain of salt. As always, be certain you understand the changes you are making.

Referral

If you would like to sign up to Bunny CDN, I would much appreciate it if you would my referral link. If you do, I will receive $20 in credits.

Tags

Niek

I build web stuff with Python and often Django. Sometimes Vue; when I am brave enough to face the frontend world.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.