Ein Megamenü zeigt schnell alle Navigationspunkte – aber ist ein Schmerz beim ersten Rendern einer Seite. Dank CachingFramework lässt sich das optimieren.
Für ein solches Menü ist es nötig, dass für alle Teilseitenbäume die Äste ausgelesen werden (TypoScript: expAll) und noch unsichtbar in HTML eingebettet werden. Bei größeren Websites quält man da schnell mal den Datenbankserver, und die Performance lässt nach. Menüs werden pro Seite generiert, und erst dann mittels Seitencache zwischengespeichert. Für große Menüs heißt dies aber, dass auch die „anderen“, inaktiven Äste jedes Mal neu generiert werden :-(
Machen wir’s mal konkreter
Ausgangssituation
Wir haben eine Website, die rund 700 Unterseiten hat, die ins Menü gelangen werden, verteilt auf sechs feste Hauptnavigationspunkte.
Zum initialen Rendern jeder Seite werden für das Menüs 700 SQL-Queries auf die pages-Tabelle abgefeuert und 700 SQL-Queries auf die realurl-Tabelle. Wir feuern also jeweils über 1400 Queries ab!
Idee
Bei der Suche nach Optimierungsmöglichkeiten wird schnell klar, dass ein großer Teil des Menüs sich ja nur selten ändert, weil nur der eigene Ast variiert. Die inaktiven Äste (also die, die gerade nicht in der Rootline enthalten sind) bleiben hingegen unverändert. Die könnte man doch cachen.
Lösung
Einen kleinen Tod müssen wir sterben: wir legen die Hauptnavigationspunkte per Seiten-ID fest. Das sollte aber bei Websites dieser Größe verkraftbar sein, da sich auf Level 1 eher selten was ändert.
Wir beginnen unser Menü als COA-Objekt bestehend aus mehreren Menüs:
lib.mainnav = COA lib.mainnav { 10 = HMENU 10 { special = list special.value = 1 1 = TMENU 1 { expAll = 1 // ... (TMENUITEMs wurden zwecks Übersichtlichkeit weggelassen) } 2 < .1 3 < .1 } 20 < .10 20.special.value = 2 // ... }
Wenn in unserer Rootline nun die PID=2 nicht enthalten ist, so könnten wir lib.mainnav.20 aus einem Cache holen, der diesen Teil des Menüs für alle Seiten bereithält, die ebenfalls nicht unterhalb der PID 2 sind.
Seit Längerem steht uns in TypoScript die cache-Funktion zur Verfügung – dann cachen wir doch einmal unsere einzelnen Menüs anhand der Level1-PIDs:
lib.mainnav { 10 { cache { key { cObject = TEXT cObject.value = 1 cObject.wrap = mainnav-| } } } 20.cache.key.cObject.value = 2 }
Prima. Das funktioniert! Nach dem Cache-leeren und dem Aufruf der ersten Seite wird jede weitere Seite deutlich schneller gerendert. Die Menü-Teile werden nicht mehr aus pages- und realurl-Tabellen mit hunderten Queries abgefragt, sondern nur noch mit einer Query auf die Tabelle des CachingFrameworks. Super – fast. Der aktive Ast wird im Moment noch gecacht, d.h. auch hiervon gibt es nur noch eine Version, die ACT- und CUR-Zustände werden falsch dargestellt.
Ein Menü soll also nicht bzw. anders gecacht werden, wenn es „active“ ist. Wir überschreiben hierfür den Cache-Identifier und setzen statt der Einstiegs-PID des Menüs die aktuelle Seiten-ID:
lib.mainnav.10.cache.key.cObject.override.data = page:uid
Das alleine reicht aber noch nicht: aktuell würden wir jetzt für jede Seite (egal in welchem Ast) das Menü separat cachen – wir würden also nichts gewinnen. Es muss also noch eine Bedingung dazu, dass der Cache-Identifier nur mit der aktuellen PID überschrieben wird, falls wir uns in der Rootline befinden:
lib.mainnav.10.cache.key.cObject.override.if.isInList = 1 lib.mainnav.10.cache.key.cObject.override.if.value.cObject = HMENU lib.mainnav.10.cache.key.cObject.override.if.value.cObject { special = rootline special.range = 0|-1 1 = TMENU 1 { NO { doNotLinkIt = 1 stdWrap.cObject = TEXT stdWrap.cObject.field = uid wrapItemAndSub = |, |*| |, |*| | } } }
Yeah! Geschafft. Aber da geht noch mehr: Warum holen wir uns die Liste der Rootline-PIDs pro Ast neu? Die Rootline bleibt doch unverändert, egal welcher Ast des Menüs gerade gerendert wird. Wir können die Rootline in ein Register auslagern, das uns dann in der gesamten Navi als Vergleichswert zur Verfügung steht:
lib.mainnav.5 = LOAD_REGISTER lib.mainnav.5.currentRootlinePids.cObject = HMENU lib.mainnav.5.currentRootlinePids.cObject { special = rootline special.range = 0|-1 1 = TMENU 1 { NO { doNotLinkIt = 1 stdWrap.cObject = TEXT stdWrap.cObject.field = uid wrapItemAndSub = |, |*| |, |*| | } } } }
Jetzt haben wir die Rootline in einem Register gespeichert, nun müssen wir sie nur noch abrufen/nutzen. Statt auf lib.mainnav.10.cache.key.cObject.override.if.value per cObject die Rootline abzufragen, lesen wir einfach das Register aus:
lib.mainnav.10.cache.key.cObject.override.if.value.data = register:currentRootlinePids
Geschafft!
Ergebnis
Durch das Cachen der inaktiven Äste des Menüs ging im konkreten Fall die Anzahl der Queries um über 1.000 zurück, wurde jede Seite initial ca 4 Sekunden schneller gerendert und verbrauchte dabei rund 40MB weniger RAM!
Nachtrag 2018-05-17
Philipp hat in seinem Snippet „Fully cached menu with TypoScript“ einen anderen Ansatz verfolgt: hier wird gleich das komplette Menü im normal-Zustand (NO) gecacht, dann aber vor der Ausgabe per stdWrap.replacement der aktive Menüpunkt (CUR-Zustand) noch hervorgehoben. Wenn man diesen Gedanken weiterverfolgt, dann sollte sich auch damit ein Megamenü samt ACT- und CUR-Zuständen bauen lassen, welches dennoch größtenteils gecacht ist.
Links
Basis dieses Artikels war eine Website auf TYPO3 7.6.23.
da frage ich mich, ob es nicht noch einfacher wäre das highlightning erst im Browser zu machen.
Idee: jeder Menüpunkt bekommt eine eindeutige ID mit (‚#M‘), dann gibt es ein Javascript, das mit der aktuellen Seitenid gefüttert wird und damit die highlighting function aufruft:
suche den menüeintrag mit aktueller id
versehe ihn mit class=“current“,
dann hangel dich rekursiv im Menü-DOM hoch und setze jeweils die class=“active“
Danke fürs Feedback. Je nach Anwendungfall stimme ich Dir zu. Das geht dann in die Richtung, die Philipp (siehe Nachtrag) auch eingeschlagen hat.
Problematisch wird dies aber, falls Du nicht nur CSS-Klassen setzen willst, sondern evtl. im active-Ast irgendwie die HTML-Struktur angepasst sein muss (z.B. weil im Menü auch Content mit eingebunden werden soll)
Hey, wir haben den Ansatz der unter „Nachtrag 2018-05-17“ beschrieben ist für uns weiter verfolgt, und verallgemeinert! :)
https://gist.github.com/julrich/d91c1ac1968418e6ae8290f2a2e90afb
Unterstützt auf die Art beliebig tiefe Menüs, Login-Bereiche, Mehrsprachigkeit und Multi-Sites. Zusätzlich zu einem „current“ konnten wir so auch „active“ verwenden.
Vielleicht hilft es ja jemandem weiter!