WordPress Performance: Caching Navigation Menus

Background

In the before time, WordPress developers used to build themes (and plugins?) using PHP. When we wanted to add a user generated navigation menu to sites, we had to use a function called wp_nav_menu(). Of course, now that we’re in the future, we don’t need to worry about such things.

But if you’re still using PHP to build WordPress sites, and still using wp_nav_menu() you might not know that these menus can be performance killers!

Problem!

Under the hood, nav menus are stored as terms in a nav_menu taxonomy. For large sites with lots of terms and taxonomies, and complex menus (custom walkers, yay!) you can really start to see menus struggle. Sure, this might be only 50-100ms, but that really adds up after millions of pageviews.

Caching Solution

One of the solutions you can do us to wrap the wp_nav_menu() calls inside a caching function, like what the Cache Nav Menus plugin does. Of course, this can complicate things, and even require you to fork third party plugins and themes to maintain caching compatability.

Another option you have is to cheat. By hooking into pre_wp_nav_menu you can cache the nav menus in place. Below is an example I’ve given customers before that shows how you can cache menus in place with a simple (mu) plugin:

/**
 * Filters and caches the output of a WordPress navigation menu.
 *
 * This function is hooked to the 'pre_wp_nav_menu' action, it generates a unique cache key
 * for every individual menu based on the menu arguments and the last time the menu was modified.
 * This key is then used to store and retrieve cached versions of the menu.
 *
 * @param string|null $output Nav menu output to short-circuit with.
 * @param stdClas     $args   An object containing wp_nav_menu() arguments.
 *
 * @return string|null        Nav menu output.
 */
function wpvip_pre_cache_nav_menu( string|null $output, $args ): string|null {
	/**
	 * Filters whether to enable caching for a specific menu.
	 *
	 * This filter can be used to selectively disable caching for specific WordPress navigation menus.
	 * By default, all menus are cached for an hour.
	 *
	 * @param bool   $enable_cache Whether to enable caching for the menu. Default true.
	 * @param string $args         An object containing wp_nav_menu() arguments.
	 */
	$enable_cache = apply_filters( 'wpvip_pre_cache_nav_menu', true, $args );

	if ( ! $enable_cache ) {
		return $output;
	}

	// Define a unique cache key.
	$cache_key   = $args->menu . ':' . md5( serialize( $args ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
	$cache_group = 'wpvip_pre_cache_nav_menu';

	// Try to get the cached menu.
	$output = wp_cache_get( $cache_key, $cache_group );

	// If the menu isn't cached, generate and cache it.
	if ( false === $output ) {
		$args->echo = false;
		remove_action( 'pre_wp_nav_menu', 'wpvip_pre_cache_nav_menu', 10, 2 );
		$output = wp_nav_menu( $args );
		add_action( 'pre_wp_nav_menu', 'wpvip_pre_cache_nav_menu', 10, 2 );
		wp_cache_set( $cache_key, $output, $cache_group, HOUR_IN_SECONDS );
	}

	return $output;
}
add_action( 'pre_wp_nav_menu', 'wpvip_pre_cache_nav_menu', 10, 2 );Code language: PHP (php)

Now, due to the weird way that you can call menus via wp_nav_menu() this might require some tweaking depending on your needs (are you using an int, string, or WP_Query to query the desired menu? Why so many choices?)

This is defaulting to caching for one hour, but you can likely cache for MUCH longer since menus rarely change. You could even cache forever and use the wp_update_nav_menu hook to purge and/or prime the cache.

Now go away and break some stuff.

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.