W3 Total Cache Optimization: Caching Post Taxonomies

Today I installed Query Monitor and noticed that each post on my homepage was triggering 2 database queries. One query retrieved the post_tag post taxonomy and one retrieved the post_format post taxonomy. Since my homepage has up to 15 posts, there can be 30 extra queries. These values should have been fetched all at once; they should not be fetched from the database individually.
WordPress not caching taxonomy
Database queries logged by Query Monitor
I disabled my plugins one-by-one and realized that the issue was caused by W3 Total Cache. Here’s how it looks like after I disabled W3 Total Cache:
W3 Total Cache disabled
Queries with W3 Total Cache disabled
There are no more queries fetching post tags or post formats for individual posts. They are batched into one query in wp_get_object_terms() (which is called by _prime_post_caches()). WordPress caches the results of the query in a WP_Object_Cache object. After digging through W3 Total Cache’s code, I realized that posts were being cached to disk while post taxonomies weren’t. When _prime_post_caches() runs, it checks if there are any posts that aren’t in the cache. If there aren’t any posts, then it doesn’t do anything. WordPress assumes to if a post is cached, then all associated post taxonomies are cached. However, W3 Total Cache didn’t cache the post taxonomies. My W3 Total Cache settings seemed to be correct. I tried finding a way to get W3 Total Cache to cache post taxonomies, but there was too much code to go through. Instead, I opted for a somewhat hacky solution.

The fix

To fix this bug, I added this snippet:

// Prime taxonomy cache. W3 Total Cache doesn't cache taxonomy, but it prevents update_post_caches() from running.
add_filter('the_posts', function($posts) {
	$ids = array_column($posts, 'ID');
	$post_types = array_unique(array_column($posts, 'post_type'));

	if (!wp_cache_get($ids[0], 'post_tag_relationships') && $post_types) {
		update_object_term_cache($ids, $post_types);
	}

	if (!wp_cache_get($ids[0], 'post_meta')) {
		update_postmeta_cache($ids);
	}

	return $posts;
}, 1);
If the post taxonomies aren’t cached, the snippet primes (prepopulates) the taxonomy cache by calling update_object_term_cache(). Since W3 Total Cache also prevents the post meta cache from being primed, I prime that as well. However, W3 Total Cache caches post meta, so it isn’t necessary to prime the post meta. After adding the snippet, my homepage went from ~30 queries to 6 queries.
After fixing W3 Total Cache issue
After inserting snippet

Checking if you have this issue

To see if you have this issue, you can download Query Monitor. You can also put something like the following in your functions.php:

if (current_user_can('manage_options')) {
    call_user_func(function() {
        $count = 0;

        add_action('loop_start', function() use (&$count) {
            $count = 0;
        });

        add_filter('wp_get_object_terms', function($terms, $object_ids, $taxonomies, $args) use (&$count) {
        	$count++;

        	return $terms;
        }, 10, 4);
        
        add_action('loop_end', function($query) use (&$count) {
            if ($count >= count($query->posts)) {
                die('Probably improper caching.');
            }
        });
    });
}
 

Leave a Reply

Your email address will not be published. Required fields are marked *