Skip to content

Instantly share code, notes, and snippets.

@DavidBruchmann
Forked from julrich/lib.navSidebar.ts
Created June 16, 2018 07:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DavidBruchmann/cf27eb309e48e0df326b3bafce2b30e3 to your computer and use it in GitHub Desktop.
Save DavidBruchmann/cf27eb309e48e0df326b3bafce2b30e3 to your computer and use it in GitHub Desktop.
Fully cached TYPO3 HMENU navigation example with expAll and 'active', 'current' states
#
# Main navigation in Sidebar
#
# General idea: Don't render & cache 'active' and 'current' states in 'expAll' menu, so it becomes cacheable
# over all pages. To regain 'active' and 'current' states, the result of the cached menu is parsed by
# 'stdWrap.replacement', utilizing specific information about the resulting menu item markup to insert them.
# Use COA to decouple the stdWrap ('lib.navSidebar.stdWrap.replacement') needed for RegExp replacement from
# the cached menu ('lib.navSidebar.10'). This way the complete menu can be generically cached without current
# and active states, but the stdWrap is still run, re-adding those
lib.navSidebar = COA
# Definition of the general menu object
lib.navSidebar.10 = HMENU
lib.navSidebar.10 {
cache {
# Use unique key for combination of '$page.uid.root' (current instance, multi-site specific)
# and the chosen language ('TSFE:sys_language_uid')
key = navSidebar-{$page.uid.root}-{TSFE:sys_language_uid}
key.insertData = 1
# 'lifetime = default' in this case refers to config.cache_period = 43200 (12 hours)
lifetime = default
}
# Start at the root of the page
entryLevel = 0
# Actual menu, crucially we render 'page_{field:uid}', e.g. 'page_123', into each relevant
# item. This gives us the option to later replace those uniquely identifiable strings to include
# additional classes like 'active' or 'current'
1 = TMENU
1 {
# 'expAll = 1' to expand all nodes recursively
expAll = 1
# Markup for items that have no submenu-items
NO = 1
NO {
wrapItemAndSub = <li class="page_{field:uid} nav-sidebar__list__item">|</li>
wrapItemAndSub.insertData = 1
ATagTitle.field = title // subtitle
ATagParams = tabindex="0"
}
# Markup for items that have submenu-items
IFSUB = 1
IFSUB {
wrapItemAndSub = <li class="page_{field:uid} nav-sidebar__list__item nav-sidebar__list__item--has-submenu">|</li>
wrapItemAndSub.insertData = 1
before = <span id="nav-sidebar_{field:uid}" role="button" aria-haspopup="true" aria-owns="nav-sidebar__submenu_{field:uid}" aria-controls="nav-sidebar__submenu_{field:uid}" aria-expanded="false">
before.insertData = 1
after = <svg class="nav-sidebar__icon"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-arrow-down"></use></svg></span>
after.insertData = 1
doNotLinkIt = 1
}
}
# Additionally add a class denoting the level for subsequent menu levels (5 supported overall right now)
# Submenu Level 2
2 < .1
2 = TMENU
2 {
stdWrap.dataWrap = <ul class="nav-sidebar__submenu nav-sidebar__submenu--level-2">|</ul>
}
# Submenu Level 3
3 < .1
3 = TMENU
3 {
stdWrap.dataWrap = <ul class="nav-sidebar__submenu nav-sidebar__submenu--level-3">|</ul>
}
# Submenu Level 4
4 < .1
4 = TMENU
4 {
stdWrap.dataWrap = <ul class="nav-sidebar__submenu nav-sidebar__submenu--level-4">|</ul>
}
# Submenu Level 5
5 < .1
5 = TMENU
5 {
stdWrap.dataWrap = <ul class="nav-sidebar__submenu nav-sidebar__submenu--level-5">|</ul>
}
}
# Add replacement stdWrap, to augment the uniquely identifable strings ('page_{field:uid} nav', e.g. 'page_123 nav'),
# embedded with the classes=".." of each menuitem, with additional classes for 'active' and 'current' states.
# the classes
# Don't add replacement function for pages where the resulting regular expression would be empty, triggering an error
# Those currently are the root page itself, and all publicly visible pages, that are not children of the root page.
[globalVar = TSFE:id={$page.uid.root}] || [globalVar = TSFE:id={$page.uid.404}] || [globalVar = TSFE:id={$page.uid.noTranslation}] || [globalVar = TSFE:id={$page.uid.search}]
# Do nothing here, we just need the negation
[else]
# Also see 'replacement' TypoScript reference:
# https://docs.typo3.org/typo3cms/TyposcriptReference/8.7/Functions/Replacement/
lib.navSidebar.stdWrap.replacement.10 {
# Construct search string of the form '#a (Cat|Dog|Tiger)#i', Cat/Dog/Tiger in this case being all the values
# we want to replace. All the menu items, to be precise their unique string (e.g. 'page_123 nav'),
# that are in the rootline need replacement here.
search.cObject = COA
search.cObject.10 = HMENU
search.cObject.10 {
# Construct a rootline menu, including all pages from the current page to the root page ('1|-1')
special = rootline
special.range = 1|-1
# Wrap the whole menu with the structure we need for the 'replacement.10.search' RegExp ('#(...)#i')
wrap = #(|)#i
# For all menuitems of this rootline, discard the actual output ('<li><a>...</a></li>')
# by setting 'doNotLinkIt = 1' and 'doNotShowLink = 1'. Generate inner part of RegExp,
# e.g. 'page_123 nav|page_1231 nav|page_2123 nav', using 'before'
1 = TMENU
1 {
NO {
# Use option split, because we don't want a '|' after the last item
before = page_{field:uid} nav| |*| page_{field:uid} nav| |*| page_{field:uid} nav
before.insertData = 1
doNotLinkIt = 1
doNotShowLink = 1
}
}
}
# Rootline looks something like this: 'root (uid: 1) > page1 (uid: 10) > page10 (uid:100) > page100 (uid:1000)'
# Only the last item in the rootline is the current item, all the items before it are active items.
# Thus we option split again, for us 'nav-sidebar__list__item--submenu-is-open' equals 'active',
# 'nav-sidebar__list__item--current' equal 'current'.
replace = nav-sidebar__list__item--submenu-is-open ${1} |*| nav-sidebar__list__item--submenu-is-open ${1} |*| nav-sidebar__list__item--current ${1}
# Enable option split for replace and RegExp for search
useRegExp = 1
useOptionSplitReplace = 1
}
[global]
# If there is a user logged in to the specific (current) instance, use a different cache key, which in addition to
# '$page.uid.root' and 'TSFE:sys_language_uid' also encodes the user uid of the logged in user, because every user
# might have his own set of visible pages, resulting in menu cache entry unique per user + instance + language.
[usergroup = {$page.uid.frontendUserGroupUid}]
lib.navSidebar.10 {
cache {
key = navSidebarLoggedIn-{$page.uid.root}-{TSFE:sys_language_uid}-{TSFE:fe_user|user|uid}
key.insertData = 1
}
}
[global]
@sypets
Copy link

sypets commented Oct 20, 2022

Thanks for providing this. I found it helpful, but a little bit complex and thus dificult to understand (not really a TypoScript friend here). I only used a very simplified version to cache the megamenu (main menu):

lib.mainMenu = COA
lib.mainMenu.10 = HMENU
lib.mainMenu.10 {
  cache {
    # Use unique key for combination of site identifier and language id
    key = mysite_mainmenu_{site:identifier}_{siteLanguage:languageId}
    key.insertData = 1
    lifetime = default
  }

   special = directory
   special.value = {$plugin.mysitemackage.mainNavPid}

   1 = TMENU
   1 {
  ....

@sypets
Copy link

sypets commented Oct 20, 2022

Some TypoScript in your example is outdated. e.g. sys_language_uid is outdated (since TYPO3 v9). You can use {siteLanguage:languageId} instead of {TSFE:sys_language_uid}.

@DavidBruchmann
Copy link
Author

Welcome, glad that it helps!
I almost forgot about this gist

@DavidBruchmann
Copy link
Author

And thanks for the proposition about the adjustment!

@sypets
Copy link

sypets commented Oct 20, 2022

I almost forgot about this gist

Yes, that's the thing with stuff you put on the Internet ;-)

There is also the b13 menus extension. I first tested that but could not really get a performance improvement out of it (but might have done something wrong).

@DavidBruchmann
Copy link
Author

I think the B13 menu is primarily for easy usability and perhaps some features but not for performance.

@sypets
Copy link

sypets commented Oct 20, 2022

b13/menus is also mentioned here https://forge.typo3.org/issues/57953 and I thought it might also be used to improve performance, but you are right, that is not really mentioned as a feature in the README.

I am still not sure about the caching - the cache entry works - as used in the code snippet above. I can debug it and set a breakpoint in VariableFrontend::get() / set() and can see the menu entry is written and reused. Also, this - in combination with getting rid of the extra states ACT etc. - does improve performance significantly in my site. Not to mention reduces the number of database queries.

However, I am not sure if TYPO3 still writes the menu cache for each page (which would now be obsolete and unnecessary).

@DavidBruchmann
Copy link
Author

I never know where the menu gets cached but couldn't it be found in the database?

@sypets
Copy link

sypets commented Oct 20, 2022

Yes, if DB is configured. I use Redis. Is stored in cache_hash.

@DavidBruchmann
Copy link
Author

Do you measure the performance somehow, or is it just a general impression that it's faster after your changes?

@sypets
Copy link

sypets commented Oct 20, 2022

Do you measure the performance somehow, or is it just a general impression that it's faster after your changes?

(I wrote how I tested - maybe you have an additional tip or this is helpful for others).

In development, I did a before / after comparison (before and after the change) and tested like this:

  • use chromium browser developer tools Network tab and select "Doc" to see just load time for the HTML page

image

  • alternatively use ab (Apache Bench) on command line
  • make sure the page is uncached, either by setting config.no_cache = 1 on a test page. This makes sure page is not fetched from cache, but the other caches, e.g. menu cache apply. Or just flush the cache for a page and make sure the menu cache is left intact.

The load times fluctuate, but the change was considerable.

page uncached without ACT etc. with menu cache
empty 1.2s .. 2.4s 1.25s 0.581s .. 0.798s
univers. 2.8s .. 2.9s 2.97s 1.33 .. 1.88

This is just the main menu optimization - I have other optimzations too which are not applied yet here. (Also the test site is a bit slow, production is generally faster, so it may not be affected as much).

Also, you can see the number of DB queries in the admin panel (Debug | Query Information). I had compared that previously when deactivating the megamenu and the DB query count dropped significantly. This is how I suspected problem in generating the menu. I did not check again with the fix, but can do that next. That would probably be more reproducible than just measuring the load times.

image


Also, it is good to see how this plays out in production, but this is a little difficult. The pages are mostly loaded cached in production and this only affects uncached. It may indirectly affect this too (because load on DB etc.), but there are too many variables, based on traffic etc.

But I have a warmup script which also outputs the load times. But this is not recorded currently.

I have a performance log in the monitoring tool (only the start page) and also in Matomo, but - again - this is not as helpful because it mixes cached and uncached results and load time is often affected by other things (such as lots of traffic due to start of semester or courses). But I can monitor performance over time - which is a good thing to do.

@DavidBruchmann
Copy link
Author

Thanks a lot for this insightful explanation, that's very interesting and especially for larger menus quite important!
Concerning any extension creating menus (like b13/menu) I suppose it could be opmizied probably by reducing the db-queries. There are certainly possibilities.
The linked code does concern only rootline menus probably but it's hopefully a clear example: https://stackoverflow.com/a/65613200/1019850

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment