Better Caching in WordPress

Caching data in WordPress is easy. Caching data in WordPress in a good and performant way takes a bit more work. For instance, many developers commonly use the Transients API to cache data. As the lowest common denominator in caching, this is okay. It’ll get the job done, even on a $10/year shared hosting plan. But what we should do instead is leverage the WP_Object_Cache functions to provide more functionality and better features.

For instance, let’s say we want to cache the result of an external API request. One way we could do this would be this way:

Note: These examples are terrible, but I hope they get the point across!

function get_api( $value) {
	$value = absint( $value );
	$api_data = get_transient( 'example-api-data-' . $value );

	if ( false === $api_data ) {
		$api_data = file_get_contents( 'https://example.com/api/' . $value );
		set_transient( 'example-api-data-' . $value, $api_data, HOUR_IN_SECONDS * 6 );
	}

	return json_decode( $api_data );
}Code language: PHP (php)

What’s one way we could make this better by using the WP_Object_Cache functions? Well, what happens if the API data structure changes, and you need to invalidate every cache value? It would be pretty hard to know the exact transient keys that you’d need to clear, and clearing the entire cache is a bit too nuclear for this (but it would work). Instead, wp_cache_*() could be used, which includes the ability to use a cache group that can be changed:

function get_api( $value) {
	$value = absint( $value );
	$cache_group = 'example-api-data';

	$api_data = wp_cache_get( $value, $cache_group );

	if ( false === $api_data ) {
		$api_data = file_get_contents( 'https://example.com/api/' . $value );
		wp_cache_set( $value, $api_data, $cache_group, HOUR_IN_SECONDS * 6 );
	}

	return json_decode( $api_data );
}Code language: PHP (php)

With this, if we ever need to invalidate the cache for this API, we just need to change the $cache_group value, and all cache requests will be new.


Another common theme I see is caching too much data. Let’s say you’re going to do a slow WP_Query, and want to cache the results for better performance:

function get_new_posts() {
	$posts = wp_cache_get( 'new-posts' );

	if ( false === $posts ) {
		$posts = new WP_Query( 'posts_per_page=5000' );
		wp_cache_set( 'new-posts', $posts );
	}

	return $posts;
}Code language: PHP (php)

Sure, that’s fine and it’ll work but… the WP_Query object is huge!

echo strlen( serialize( new WP_Query( 'posts_per_page=500' ) ) ); … 2,430,748

That’s 2.5 megs of data needing to be transferred out of cache on every pageload. If your cache is accessed across the network on another server, this introduces more delay as it has to transfer. Also, some caching solutions might put a limit on the size of an individual cache object–which means that an object like this might never be cached!

Instead, we can just grab the IDs of the posts, and do a second, much faster query:

function get_new_posts() {
	$post_ids = wp_cache_get( 'new-posts' );

	if ( false === $posts ) {
		$post_ids = new WP_Query( 'posts_per_page=5000&fields=ids' );
		wp_cache_set( 'new-posts', $posts->posts );
	}

	$posts = new WP_Query( [ 'post__in' => $post_ids ] );

	return $posts;
}Code language: PHP (php)

echo strlen( serialize( $posts->posts ) ); … only 88,838 bytes, that’s like a 96%-something difference!

I had a few more ideas for this post, but it’s been sitting as a draft forever and I don’t remember. It’s possible this topic might be revisited some day 🙂

Other Posts Not Worth Reading

Hey, You!

Like this kind of garbage? Subscribe for more! I post like once a month or so, unless I found something interesting to write about.


Comments

4 responses to “Better Caching in WordPress”

  1. It would be pretty hard to know the exact transient keys that you’d need to clear, and clearing the entire cache is a bit too nuclear for this (but it would work). Instead, wp_cache_*() could be used, which includes the ability to use a cache group that can be changed

    If you need to keep using Transients, you can just store the group as a transient and append it to the key. Then, to invalidate, just change the group transient.

    1. Oh, that’s a smart idea! I think one thing that people would need to be careful about is making sure that the key doesn’t exceed the character limit in the database by appending the group name.

      (More details for anyone playing at home: https://codex.wordpress.org/Function_Reference/set_transient#Parameters)

  2. Nice explainer and some great tips too.

    If I understand it correctly, wp_cache functions make use of the object cache. On low-quality hosting, that may not be an option – only transients guarantee caching, despite the simplicity of it. Or do you feel the use of object caches, outside of the business-level of site management, is prevalent enough that this can ignored these days? i.e. when do you feel that transients should be used in preference?

    1. The wp_cache functions are available without a persistent object caching plugin, but they … don’t persist are are only stored in memory during each page load.

      It’s kind of a tough choice to make. There’s a couple options if you want to get super crazy, like creating a wrapper class in a plugin that will either choose transients or the wp_cache functions, depending on if you’re using a persistent cache or not… but for most cases, it’s probably overkill.

      I think transients are nice for any unique one-off bits of data that aren’t connected to a larger group of data, or if you do need caching with the highest amount of compatibility across the WordPress ecosystem.

Leave a Reply