Websites often have very large menu trees, also called mega menus. They contain dozens or hundreds of different menu links that are usually rendered up front, so that their trees or expanded displays are immediately visible when visitors click on them.
Achieving good performance with regular Drupal menu blocks can be challenging for three reasons:
- Expensive to build
- Many variations
- Frequently invalidated
A good example for these challenges is the https://www.psi.ch/en website (See our case study if you want to know more):
- Between the main navigation, the top header and footer menus, there are about 10 different menu blocks displayed on any given page
- The site has tens of thousands of content pages in dozens of different sections, many of which have their own version of the main navigation. These sections use the Group module to manage editor access.
- 4 languages
- A visible active trail that indicates the currently active main navigation section as well as the child links below
- The active trail also needs to be vary not only for the pages that are directly linked, but content like articles, events and projects also need to provide the appropriate active trail for their respective overview page. We use Menu Trail by Path for that.
In the last few minor releases, there have been significant improvements in Drupal core and as well as some contributed projects, but there's still work to do.
This article will delve deeper into the 3 challenges with examples and completed and proposed improvements for each. Some improvements were done specifically for this sites, many others are now available to everyone partially due to our involvement, but some require appropriate configuration.
Expensive to build
On many Drupal sites, most or all of these menu links point to nodes or another type of entity. Drupal will automatically check whether or not the target entity is accessible for the current user, by default that happens one by one. That means each menu link loads the target node and does access checks on it, which may trigger fairly expensive hooks, such as node grant lookups, or in the case of PSI, a reverse lookup of the group relationship pointing to that node, then loading the group relationship and then its group.
PSI also contains additional elements in the different menu sections as well as barely noticeable features that are quite expensive to maintain. One is the small arrows, whose direction indicates if the link target goes to a different section (read: different main navigation). That means the group of the target node needs to be loaded and the configured menu compared with the one of the current node.
Using Blackfire and manual profiling, we identified that building the main navigation took up to 50% of the total processing time, even on complex pages. with many paragraphs, embedded views and so on.
Improvements
Many of the improvements we did for this aspect are site-specific, but there are some related core issues as well.
Variations
When using visible active trail indicators, these menu trees also need to be built different for every page. The active trail is meant to show visitors in which section of a website they are currently in. For example, a news article may be located below a News, which is with then About Us section of your main navigation. This is similar to the breadcrumb, but may not follow the same rules.
Until recently, with just Drupal core, every menu block had a cache context that essentially varies for every possible active menu link, even when no CSS exists that would do anything with the the automatically added CSS classes.
The Menu Trail by Path module makes active trail calculation more expensive, as it essentially varies by every possible URL path.
For PSI, the combination of active trails, 4 languages, dozens of different menus means that we currently see around 5000 variations of the main menu block in our cache. These statistics can be explored with the statistics provided by the Redis module, either the report in the UI, or starting with Redis 2.x, also a Drush CLI command.
Improvements
Invalidations
As if expensive menu trees that are built and cached in dozens to thousands of variations wouldn't be bad enough, they also tend to be frequently invalidated on sites that update content linked in their menus semi-frequently for two reasons:
- Until now (before Drupal 11.5), saving any node with an active menu link through the menu_ui module always saved, rebuild and invalidated any rendered versions of that menu as well as any rendered page that contains that menu. Even if no relevant change was made to the to menu link or anything that would affect its visibility.
- Access checks for users that do not have the bypass node access permission add the node:ID cache tag. That means that a menu that links to 100 node pages will have 100 different node:ID cache tags associated with it. Even when the menu link is not saved, such as updating the node through the API.
The result is that every time a page in the menu is saved, all already cached variations that link to that page are invalidated and need to be rebuild again.
Improvements
Conclusions
Since Drupal 11.3, there have been several improvements in Drupal core to improve the menu performance, but most require some configuration, additional modules or at least awareness of the problems that your site may face.
We will expand this post with other contributed projects or core issues to improve menu performance, share them with us.