Card with the words "Reduce", "Reuse" and "Recycle"

Globally cached fragments in Neos CMS

Categories:

Neos CMS and the Fusion rendering DSL (Domain-specific language) allow very detailed and reliable way of caching pages, content elements and even the tiniest fragments and values. 

But really helpful for the rendering performance is caching of larger fragments that need a relatively long time to generate and appear identically on more than one pages of your site.

Examples:

  • Main navigation
  • Page footer
  • Latest news listing
  • Recipe of the day

How to define a globally cached fragment

Often the current documentNode is part of the entry identifiers.

This means the cached entry is specific to that node / page.

So instead you would add some static identifier like „page-footer“ and for an editable element the node in which the content is edited in.

Now when two pages of the same type are generated, the second one can reuse the already cached „page-footer“. Yay speedup!

There is a catch

When investigating the caching behaviour with a tool like t3n/neos-debug you might see that the footer on your homepage and the footer on your contact page have different entryIdentifier hashes.

The reason for this is that the fusionPath of the rendered element is the final ingredient that will be added to the concatenated identifiers already defined by the GlobalCacheIdentifiers and defined by your own cache configuration.

This means, if your homepage has the nodetype My.Site:Homepage and your contact page has the nodetype My.Site:Page the fusionPath will be different and therefore a different hash is generated leading to two separate cache entries.

That’s necessary for Fusion to render exactly what you want but sadly not very obvious.

The context

Recently there has been a discussion which made me realise that some projects I’m involved in and even the Neos.Demo don’t do this correctly. Meaning the page rendering on those sites is not as fast as it could be (when a page is uncached).

So how do we solve it?

By rendering the element that we want to have available everywhere into an absolute rendering path independent of any parent node type involved in the rendering.

So here is the footer you might know from the Neos.Demo site.

prototype(Neos.Demo:Document.Fragment.Menu.Meta) < prototype(Neos.Fusion:Component) {
    menuItems = Neos.Neos:MenuItems { ... }

    renderer = afx`...`

    @cache {
        mode = 'cached'
        entryIdentifier {
            static = 'metamenu'
			site = ${site}
        }
        entryTags {
            1 = ${Neos.Caching.nodeTypeTag('Neos.Neos:Document', site)}
        }
    }
}

As you see there is a static entry identifier and the site. Including the site allows us to have separate cache entries based on the workspace and the dimension (e.g. language) the site node is in.

Without it we would get the same menu for each language.

But as mentioned before the meta menu would now have a different fusionPath depending on the document node type it’s part of.

To fix this we rename the prototype like this:

prototype(Neos.Demo:Document.Fragment.Menu.Meta.Renderer)

And we render the meta menu into an absolute path:

metaMenu = Neos.Demo:Document.Fragment.Menu.Meta.Renderer

Great. Now we would get an error if we try this because in our page prototype we try to render the old meta menu prototype.

So we define it again in a very simple way:

prototype(Neos.Demo:Document.Fragment.Menu.Meta) < prototype(Neos.Fusion:Renderer) {
    renderPath = '/metaMenu'
}

Hurray. Our meta menu works as before and if you use a debug tool to inspect the cached elements you will see that the same cached meta menu is now used on all page types referencing it.

Note: As the site is also included in the entryIdentifiers the cached entry automatically respects the current dimension and other context parts of the site node.

But why does it work now?

The meta menu now has a static fusionPath not depending on any other prototypes and via the Neos.Fusion:Renderer we can call this path and insert the result wherever we need it.

Are there any disadvantages?

Don’t use this concept for everything.

For example you should not try to access anything page specific like the documentNode in the globally cached element. That would break the whole idea again and you should rather do it the „old“ way. The same page types would still reuse the same cached element.

Be careful what else you access in your element that you want to cache and make sure you test it well for side-effects.

Any other hints?

In the already mentioned discussion Sebastian Flor showed a helper to „standardise“ their globally cached elements in their projects. I haven’t tried this exact code yet, but it might be helpful.

Besides the code shown above is short enough, so you might not need anything else. It works the same way for most elements.

I mentioned the main navigation as globally cached element. But you might wonder why I do that, as most navigations show the currently selected path or item.

As the rendering of a navigation can become quite slow in many projects I recommend in most cases to set the active states of a navigation via javascript.

You can traverse through the items and modify their classes based on the current location.

This is a great way to reduce the number of variants of your main navigation to one per dimension instead of having as many rendered navigation cache entries as you have pages (multiplied by your dimensions and possibly workspaces.

And the last hint: use the latest versions of t3n/neos-debug as I added the fusionPath visually to the entryIdentifiers for each cache segment.

Summary

Globally cached fragments can be a big boost to your sites rendering performance for uncached pages.

If your components are too complex and use page specific parts, maybe slice them into smaller pieces. Try to have a big one that can be cached and have a separate cache config for the page specific part. Or load the necessary information via JS.

I would love your feedback, maybe you even have a better solution for cases like this :)