Inpsyde Menu Cache: The Secret Art of Caching WordPress Menus

menue cache in WordPress

This post introduces the Inpsyde Menu Cache plugin, which lets you cache and re-use rendered WordPress navigation menus.

Our second door of our advent calendar!

There are several things you can do or use to speed up your WordPress website—one of it is caching. But caching is not a concrete thing in and of itself. It is a technique that you can use in various ways, on various levels, and with various kinds of data. This post introduces the Inpsyde Menu Cache plugin, which lets you cache and re-use rendered WordPress navigation menus. The plugin thus provides caching of HTML fragments, and it is quite configurable at that.

Why Use a Menu Cache?

Unless you are already using some kind of HTML fragment cache or even a full-page cache, every front-end request to your website will trigger the markup generation for all your navigation menus. Depending on what you put into these menus, this can be a tremendously costly action, which, again, is being performed for every single page visit all over again.

Wasting Time

Under the hood, WordPress renders navigation menus via the wp_nav_menu() function. Internally, this leads to a call of _wp_menu_item_classes_by_context(). Depending on the context, this function then calls wp_get_object_terms()for every menu item—which in turn will issue lots of database queries that are not cached.

Consequently, each and every menu will get generated from scratch, for each and every visitor. Assuming you don’t change all your menus every other five minutes, this is a huge waste of time, which is completely unnecessary.

Real-World Example

In the course of a larger WooCommerce project, I was given the task to profile and speed up the front-end experience. By using the awesome Query Monitor plugin—in particular its handy database-specific analysis functionality—and the Xdebug profiler, I got rid of several duplicate queries, and I also circumvented quite costly and uncached queries (e.g., by making the theme pass WP_Post objects instead of plain IDs, which almost all of the receiving functions only used to fetch the according objects on their own).

Another large amount of the time spent, however, was when rendering navigation menus. By the way, the main menu was a mega menu with more than 70 items, and for every item, third-party plugins were running several custom queries, uncached.

This is an easy thing to spot, and luckily an easy one to fix, too. So that’s what I did.

What Does Inpsyde Menu Cache Do?

The plugin leverages two filter hooks provided in the mentioned wp_nav_menu() function. One to store a freshly generated menu (i.e., the rendered HTML) in the cache, and one to check for a cached menu before generating it (anew). In fact, we would be fine with an action hook after successful generation of the menu markup, but as there is none, we have to (mis)use another filter hook.

Menu Cache Lookup

For every menu that is to be rendered, the plugin intercepts the program flow and checks if there already is a cached version of the menu waiting. If there is, it will be used, no questions asked.

Caching Menus for Re-Use

Every time WordPress did generate new menu markup, it will be cached so it can be re-used later, and so subsequent page loads will be faster.

Each item in the cache is stored with a unique key, but it’s up to you to decide what will end up as an individual item. By default, the plugin will cache every unique usage of a menu. This means that if you render two or more times the exact same menu with the exact same configuration (i.e., the arguments passed to wp_nav_menu()), these menus will share a room in the cache, no matter what URL, user or other possible context variable.

How to Use Inpsyde Menu Cache?

The following section gets you started with the plugin, and it also shows a few usage examples of the provided filters.

Installation

Inpsyde Menu Cache requires PHP 5.4 or higher, although we all know you should be using PHP 7, right? 😉

So far, you can not download the plugin from the WordPress Plugin Directory. Instead, we chose to make it available as Composer package. This means you need Composer to install the plugin:

$ composer require inpsyde/menu-cache

By the way, if you want to manage complete WordPress websites with Composer, we can only recommend the awesome WP Starter package.

What About Dynamic Menus?

Like mentioned before, the plugin treats menus irrelevant of the current URL. If you are using a walker that generates different output based on the current URL (or queried object etc.) or if your theme includes dedicated styles for special menu items (e.g., the current menu item or one of its ancestors), you might want to adapt the key generation accordingly.

Here is a simple example of using individual cache items for every single URL:

<?php

namespace My\MenuCacheCustomization;

use Inpsyde\MenuCache\MenuCache;

add_action( 'plugins_loaded', function () {

    if ( ! class_exists( MenuCache::class ) ) {
        return;
    }

    add_filter( MenuCache::FILTER_KEY, function( $key ) {

        $suffix = md5( $_SERVER['REQUEST_URI'] );

        return "{$key}_{$suffix}";
    } );
} );

What About Invalidation?

By default, the plugin does not invalidate any cached data, but makes use of expiration, which is by default 5 minutes. Although possible, cache invalidation is quite hard, and even harder in this case where the generation of the keys is filterable. If you really need invalidation here, you might be doing caching the wrong way, or should not be caching that particular menu at all…

That being said, invalidation can be retrofitted by implementing something into the key generation that either can be invalidated manually or will auto-invalidate. A very rudimentary example could look like this:

<?php

namespace My\MenuCacheCustomization;

use Inpsyde\MenuCache\MenuCache;

const OPTION = '_latest_menu_update';

add_action( 'plugins_loaded', function () {

    add_action( 'wp_update_nav_menu', function () {

        update_option( OPTION, time() );
    } );

    if ( ! class_exists( MenuCache::class ) ) {
        return;
    }

    add_filter( MenuCache::FILTER_KEY, function( $key ) {

        $timestamp = get_option( OPTION, time() );

        return "{$key}_{$timestamp}";
    } );
} );

In the above code, we use a dedicated option to store the timestamp of the latest menu update—any menu. This timestamp is being incorporated into the key generation, and so the key for the exact same menu will change with every menu change. Now we could boost the expiration time to several hours or even days, if we wanted.

Again, this might not be the best invalidation scheme, and good ones always strongly depend on the key generation anyway.

Can I Disable the Cache for $reason?

Sometimes, the cache can be annoying. Say you are debugging your stage/production server, or you don’t want certain users to get data served off the cache. Luckily, you can disable caching altogether based on whatever condition you can come up with.

Here is the debugging example:

<?php

namespace My\MenuCacheCustomization;

use Inpsyde\MenuCache\MenuCache;

add_action( 'plugins_loaded', function () {

    if ( ! class_exists( MenuCache::class ) ) {
        return;
    }

    add_filter( MenuCache::FILTER_SHOULD_CACHE_MENU, function( $should_cache ) {

        return defined( 'WP_DEBUG' ) && WP_DEBUG
            ? false
            : $should_cache;
    } );
} );

Inpsyde Menu Cache is Open Source!

As supporters of the WordPress community as well as Open Source in general, we made Inpsyde Menu Cache a plugin that is both free to use and easy to contribute to.

In case you have a feature request, found a possible bug, or just don’t understand a thing regarding the plugin, hop on over to its GitHub repository and create a new issue. Needless to say, we are also more than happy about any kind of pull request you might want to send over to us, be it fixing a tpyo typo, enhancing the documentation, adding some more tests, or even implementing a new feature you find important. Before doing any of this, however, please check if there already is a conversation about what you are going to report.

Also, comments are open. 😉

Happy Caching!

Open the third door of our WordPress Advent Calendar 2017!