WordPress Caching - All you need to know

WordPress Caching: All You Need To Know

Over the last few years of supporting our products at Delicious Brains (notably WP Offload Media and most recently, SpinupWP), one thing has become clear:

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:

  1. It improves application performance. For WordPress sites, this means that your site loads faster.
  2. 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).

Caching Layers

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:

WordPress caching before

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:

  1. Browser caching
  2. Page caching
  3. Object caching

WordPress caching after

Let’s dive into each of those layers. We’ll work from the outside in, which brings us to browser caching.

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).

Browser cache before

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.

Browser cache filter

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:

  1. Assets which are browser cached aren’t transferred.
  2. 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.

Browser cache after

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 cache-control and 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

Typically, the 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.

Common Gotchas

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:

https://spinupwp.com/wp/wp-includes/css/dashicons.min.css?ver=5.2.2

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!

3. The cache-control and 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

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.

Caching requests per second

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.

Caching average response time

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.

Common Gotchas

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.

Object Caching

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:

Object cache disabled

With object caching enabled, this drops to 2 database queries and reduces page generation time from 0.085s to 0.053s:

Object cache enabled

These tests were performed using PHP 7.3, a clean install of WordPress 5.2.2, WooCommerce 3.6.4 and the Storefront theme.

Common Gotchas

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.

Author

Ashley Rich

Ashley is a PHP and JavaScript developer with a fondness for hosting, server performance and security. Before joining Delicious Brains, Ashley served in the Royal Air Force as an ICT Technician.

100% No-Risk 30-Day Money Back Guarantee

If for any reason you are not happy with our product or service, simply let us know within 30 days of your purchase and we'll refund 100% of your money. No questions asked.