Getting wiser about Craft CMS caching (version 2.6)

A small case study on what we did to optimize our use of caching in Craft CMS 2.6.

This post builds upon my previous post: Craft CMS and the Need for Speed.

Insert personal anecdote #

I helped build a multi-site craft installation which was meant to support more than seven sites. It worked fine for a while but when multiple users started writing articles we started to experience a lot of performance degradation. Basically it took a long, long time to save articles. And it puzzled us greatly.

I searched craftcms.stackexchange.com (as programmers tend to do) and I started wondering if we might have encountered an issue with a task called DeleteStaleTemplateCaches. It’s a cleanup task that is run when an article is saved.

After researching this answer and this answer I got the idea to check out a database table called templatecaches. Right beside that table I also found tables called templatecacheelements and templatecachecriteria. I then went and had a look at what table rows each of these tables contained.

Checking out the table’s data did not make me that much wiser but I did notice that each of these tables contained quite a few rows.

I also searched our template files to figure out where exactly we we’re using cache tags. And I finally discovered that we used cache tags twice in only one template file. But that in turn was a _layout.twig template which was used as basis for nearly all the pages.

The original code #

{% cache if craft.config.cache %}
  {% include 'modules/header' %}
{% endcache %}

<main class="c-page" id="top">
  {% block content %}
  {% endblock content %}
</main>

{% cache if craft.config.cache %}
  {% include 'modules/footer' %}
{% endcache %}

I stared at this code for a little while and considered the rows contained in the templatecaches table. It was 160 something rows. These rows looked very similar to the point where they seemed accidentally duplicated. Considering the code above one would think that they idea is to cache header html and footer html exactly once per language locale per site.

So I had a second look at the Craft CMS documentation on caching. And I found the globally keyword.

Second version #

{% cache globally if craft.config.cache %}
  {% include 'modules/header' %}
{% endcache %}

<main class="c-page" id="top">
  {% block content %}
  {% endblock content %}
</main>

{% cache globally if craft.config.cache %}
  {% include 'modules/footer' %}
{% endcache %}

I deployed the updated template code. Afterwards I logged into Craft’s admin dashboard and cleared the template cache in the settings. I then navigated around the site for a bit before checking out Craft’s caching tables in the database. All the tables were smaller and the templatecaches table contained two rows representing saved html content for the header and footer. I experimented some more by switching site language and visiting some of the other sites. A few more rows were generated as expected.

The optimization journey could have ended here. But for our distinct use case I was wondering if we could gain even more performance by specifying cache keys.

Third (and current) version #

{% cache globally using key siteKey ~ "-header" if craft.config.cache %}
  {% include 'modules/header' %}
{% endcache %}

<main class="c-page" id="top">
  {% block content %}
  {% endblock content %}
</main>

{% cache globally using key siteKey ~ "-footer" if craft.config.cache %}
  {% include 'modules/footer' %}
{% endcache %}

So what’s happening here?

The globally keyword specifies that we’re caching some content once per locale. Adding to that we include the using key which replaces the randomly generated key with our own custom key.

“If [cache key] is not provided, a random key will be generated when Twig first parses the template (source).”

For our use case I chose a key specific to the site currently viewed. This made our caching even more disciplined. Meaning that there is now simply no chance of generating any redundant cache rows. There will only be one footer plus one header for each language locale and each site.

I must admit I might be pushing it at this point. Because there is a caveat.

“If you change the template code within a {% cache %} that uses a custom key, any existing template caches will not automatically be purged. You will either need to assign the tag a new key, or clear your existing template caches manually using the Clear Caches tool in Settings (source).”

We’ll have to see over time whether or not using key was wise.

Editing and saving articles is finally fast. Which was our goal to begin with. From this point on we’ll just have to measure and tweak if need be. Maybe we’ll discover that the code in the second version would suit our needs better. Or maybe not.

So the morale to this story is iteratively measure and tweak.

But do avoid making premature optimizations. Optimizations that make code hard to read and change should be your very last resort. A better solution might be to invest in faster server hardware for example.