WordPress Caching: All You Need To Know
Caching in WordPress is often misunderstood (and sites run slower than they should as a result).
In this post, I hope to demystify some of those misconceptions and provide a little clarity on the minefield that is WordPress caching. Hopefully, by the end of this post, you’ll have a better understanding of how the various layers of caching play a role in turbocharging WordPress.
Why is Caching Important?
Before we dive into the various caching mechanisms, it’s essential to understand the benefits of caching. Caching plays two major roles:
- It improves application performance. For WordPress sites, this means that your site loads faster.
- It increases application throughput. Meaning, your site can handle more traffic.
What’s more, caching can increase both application performance and throughput without increasing hosting costs. This is because you need far less system resources (CPU & memory) to host a site that has been correctly cached. It really is a win-win strategy (when done correctly).
WordPress is a database-driven CMS, meaning there are many moving parts when handling an incoming request. Out-of-the-box WordPress has to query the database and render the page before it can be sent to a user. This happens on every single incoming request, which is hugely inefficient if the page content hasn’t changed. A typical request will look something like this:
As a general rule, the more moving parts involved with handling a request, the longer a user has to wait for a response and the more system resources that are used. To combat this, caching is often performed in layers, with each layer sitting in front of a moving part. The three prominent layers in WordPress are often broken down to:
Let’s dive into each of those layers. We’ll work from the outside in, which brings us to browser caching.
Although browser caching doesn’t necessarily help with application response time or throughput (at least in the realms of WordPress), it is the most important layer when it comes to reducing the amount of data that must be sent from the server to the browser. This can make your site feel much more responsive because static assets such as CSS, JS, and images appear much quicker if they’re cached by the browser.
When it comes to understanding the browser cache, the network tab in your browser’s developer tools is your friend. Let’s take a look at the browser cache in action, by loading the SpinupWP site as an example (because it’s shiny and new). The first page load won’t have any assets cached by the browser (I’ve turned on ‘Disable cache’ to spoof an initial page load).
The metrics we’re mostly interested in are the amount of data transferred and the total resources.
1.2 MB / 1.2 MB transferred 1.9 MB / 1.9 MB resources
First, be aware that each metric has two values (1.2 MB / 1.2 MB) because you can filter the results by resource type. This allows you to easily see which type of resources is taking up the majority of bandwidth.
Second, the transferred value should always be lower than the total resources, even if this is the first time the page has been loaded by the browser (or ‘Disable cache’ is enabled), for two reasons:
- Assets which are browser cached aren’t transferred.
- Assets such as HTML, CSS, and JS should be compressed by the server before being transferred to the browser. They’re then uncompressed by the browser before being displayed to the user.
Therefore, if browser caching is correctly configured subsequent page loads should transfer less data. You can see this for yourself when we reload the page.
Data transferred has dropped from 1.2 MB to 36.9 KB, which is a huge saving! You’ll also notice that the load metric has dropped from 874ms to 355ms.
Browser caching behavior is controlled by the
expires headers which are added to the response of static assets by your server. The
cache-control header will take precedence over the older
expires header. But, both are usually applied to ensure compatibility with some legacy proxy services.
You can check an asset’s headers using the developer tools, or using the cURL command:
$ curl -I https://spinupwp.com/wp-content/themes/spinupwp/assets/images/video-screenshot.png HTTP/2 200 server: nginx date: Wed, 26 Jun 2019 09:25:43 GMT content-type: image/png content-length: 46753 last-modified: Tue, 18 Jun 2019 17:57:57 GMT etag: "5d092625-b9c6" expires: Thu, 25 Jun 2020 09:25:43 GMT cache-control: max-age=31536000
cache-control header will have a value of
max-age=<seconds> which will instruct the browser to cache the file for a maximum of
<seconds> and prevent it from being re-downloaded on subsequent requests. In the case above, the file will be cached by the browser for up to 1 year.
1. Cache invalidation is difficult. Once a browser has cached an asset using
cache-control: max-age=<seconds> you can’t tell the browser to uncache it, at least not without changing the URL. This is why WordPress adds a version query string to enqueued assets. For example:
2. Website speed test tools such as Pingdom will complain about short-lived expiration headers. Often, these warnings are triggered by external assets like Google Analytics. This is because external services have to use short expiration times to ensure their assets are up to date due to the difficulties with cache invalidation. With a far-future expiration time you would have to update your embed scripts every time Google Analytics changed their code. Ugh!
expires headers aren’t the only headers that the browser uses to determine whether a resource should be fetched from the server. For example, once the duration specified by
cache-control: max-age=<seconds> has elapsed the
etag header will be used. This contains a validation token (similar to an MD5 hash of the requested resource), which is compared to the token of the resource stored on the server. If the
etag value is the same, the resource returns a “304 Not Modified” response. This tells the browser that the cached resource hasn’t changed and that it renews the expiration time specified in
cache-control: max-age=<seconds>. HTTP caching is an entire subject in itself, but you can read more about it here.
Page caching is going to give you the most bang for the buck when it comes to improving both application response time and throughput in WordPress. A page cache essentially turns WordPress (a database-driven CMS) into a static HTML site by taking both PHP and MySQL out of the equation when it comes to handling a request.
To demonstrate how significant an impact page caching can have, I’m going to benchmark a clean installation of WordPress 5.2.2 using ApacheBench. In these tests I’m using SpinupWP to provision a 8 GB, 4 vCPUs DigitalOcean Droplet tuned for hosting WordPress sites. The uncached results are for WordPress with no caching configured (except PHP OPcache, using PHP 7.3 default values). The cached results have SpinupWP’s one-click page cache enabled.
Let’s start with requests per second (higher is better), which translates to:
It increases application throughput. Meaning, your site can handle more traffic.
That’s a 10x increase in requests per second, which is awesome! But, a high requests per second doesn’t mean much if those requests are slow to complete. That’s why it’s important to also measure the average response time (lower is better), which brings us to:
It improves application performance. For WordPress sites, this means that your site loads faster.
Average response time is halved when the page cache is configured, which is pretty remarkable considering 10x the amount of requests per second are being handled.
1. Page caching is extremely difficult when sites display personalized content like e-commerce sites or are highly dynamic like forums. Often, the page cache will need to be completely disabled or specific pages like the
/checkout will need to bypass the cache.
2. Cache invalidation can be tricky depending on how it’s implemented. Due to the dynamic nature of WordPress, it’s difficult to determine which pages should be purged from the cache when content is updated (especially when archives, pagination and widgets are involved). This is why purging the entire page cache is often the preferred approach when content has changed.
3. Using multiple page cache solutions won’t improve performance. In fact, having multiple page caching solutions is highly discouraged because it can cause cache incoherency (different versions of your site stored in each cache) and make cache invalidation difficult.
4. Page caching is usually disabled for logged in users, which is why object caching (below) is still advisable.
As mentioned previously, not all pages can be page cached. This is especially true of e-commerce and membership sites, which often display personalized content. It’s also true of the WordPress admin area. If such dynamic pages were page cached, users would see personalized content not relevant to them.
WordPress has object caching built-in, which allows data such as database queries to be stored in memory. This is how multiple calls to functions such as
get_posts only results in a single database query. However, the object cache is non-persistent by default (meaning it doesn’t live beyond a request). Luckily, WordPress can be integrated with a persistent data store such as Redis, which is crucial for scaling dynamic pages. The object cache sits between PHP and the database which speeds up PHP execution time and reduces the load on the database by caching queries in memory. You can see the impact an object cache has by installing the Query Monitor plugin. Loading the WooCommerce cart page without object caching enabled results in 32 database queries:
With object caching enabled, this drops to 2 database queries and reduces page generation time from 0.085s to 0.053s:
These tests were performed using PHP 7.3, a clean install of WordPress 5.2.2, WooCommerce 3.6.4 and the Storefront theme.
1. Additional server software such as Redis or Memcached is required to make the object cache persistent. This should be taken care of if you’re using a good WordPress host, or you can install it yourself if you manage your own server.
2. The object cache doesn’t completely remove the reliance on the database, which is often the biggest bottleneck in WordPress. This is because the SQL queries to build the post index will always be executed as the results are not cached.
That’s All Folks
I’ve only scratched the surface of caching as it relates to WordPress. Hopefully, you have a better understanding of how the various caching layers work. If you would like to dive deeper, I recommend checking out HTTP Caching which gives more insight into browser caching. WordPress as Scale is also an excellent resource for learning more about WordPress specific performance.
Still not sure about WordPress caching? Ask away in the comments below.