For coders TYPO3 Tech Corner

Dropdown-Menü ohne Last - mittels Multilevel-Cache

Dropdown-Menü ohne Last - mittels Multilevel-Cache

Im Artikel "Menü: Vergleich der Techniken" hatten wir verschiedene Arten und Varianten verglichen, um ein Mega-Menü zu bauen. Die Variante des TypoScript-basierten Menüs mit Cache auf mehreren Ebenen hatten wir aus Platzgründen aber bisher nur zahlenmäßig und mit groben Konzept vorgestellt. Jetzt gehen wir Schritt für Schritt durchs Menü und optimieren es mit Caching auf Ebene 1 und 2.

Hinweis:
Alle Snippets sind der Übersicht halber vereinfacht und gekürzt.
Der vollständige Code findet sich im Git-Repository des Test-Projektes (Variante 5).

Ausgangbasis

Als Basis der Überlegungen diente das Menü aus meinem alten Artikel "Mega-Menü mit Cache optimieren". In vereinfachter Form (und noch ohne Cache) sieht es so aus:

10 = COA 10 { 10 = HMENU 10 { special = list special.value = 3 1 = TMENU 2 = TMENU } 20 < .10 20.special.value = 189 30 < .10 30.special.value = 4 }

Schon damals fiel ein Punkt negativ auf: "Einen kleinen Tod müssen wir sterben: wir legen die Hauptnavigationspunkte per Seiten-ID fest.". Im konkreten Projekt, für das die Navigation diesmal optimiert werden musste, konnte dieser Nachteil nicht hingenommen werden. Denn die Hauptseiten konnten durch die Redaktion auch mehr oder weniger werden.

HMENU ist mit Cache, TMENU nicht

Hintergrund der Aufteilung in einzelne HMENU-Objekte war, dass diese eine cache-Eigenschaft haben, während TMENU-Objekte diese nicht haben. Dem TMENU wurde in TYPO3 v12 auch der interne (ineffiziente) Cache genommen (#98964).

Wir brauchten also ein flexible Lösung, um einzelne HMENUs zu bekommen.

Da gab's doch mal.... doNotLinkIt

Nach einiger Überlegung kam die passende Idee: ein HMENU, das seine Menüpunkte der Ebene 1 aber auch wiederum als HMENU generiert.

Wer früher™ schon Menüs mit TypoScript gebaut hat, der erinnert sich vielleicht noch an doNotLinkIt als Eigenschaft des TMENUITEM. Damit lässt sich das "normale" Verhalten der Menügenerierung aushebeln, und man kann individuelle Dinge zum Anzeigen der Menüpunkte bauen (Ein Beispiel wäre z.B. eine Menüpunkt, der auch ein Bild aus den Seiteneigenschaften mit anzeigt).

Das nutzen wir, um hier - satt eines Links zum Menüpunkt - gleich via stdWrap.cObject ein ganzes HMENU einzubauen. Und die Einstiegsseite für das untergeordnete HMENU bekommen wir aus den "Feldern" des aktuellen Objektes, also dem TMENUITEM:

10 = COA 10 { 10 = HMENU 10 { 1 = TMENU 1 { NO { doNotLinkIt = 1 stdWrap.cObject = COA stdWrap.cObject { 20 = HMENU 20 { special = list special.value.field = uid 1 = TMENU 1 { //... } 2 = TMENU 2 { //... } } } } } } }

Nachdem in unserem Projekt der Cache auf der ersten Ebene nicht ausreichte, benötigten wir die HMENUs auch noch eine Ebene darunter. Nach den obigen Überlegungen ist das dann nur noch Fleißarbeit:

10 = COA 10 { 10 = HMENU 10 { 1 = TMENU 1 { NO { doNotLinkIt = 1 stdWrap.cObject = COA stdWrap.cObject { 20 = HMENU 20 { special = list special.value.field = uid 1 = TMENU 1 { expAll = 1 } 2 = TMENU 2 { NO { doNotLinkIt = 1 stdWrap.cObject = COA stdWrap.cObject { 20 = HMENU 20 { special = list special.value.field = uid 1 = TMENU 1 { //... } 2 = TMENU 2 { //... } } } } } } } } } } }

Cache: Level 1

Je früher wir cachen können, desto besser. Am besten wäre also, wenn wir einen Hauptmenüpunkt samt seiner Unterpunkte gar nicht neu bauen müssten, sondern komplett aus dem Cache bekommen könnten.

Wie schon im alten Artikel erklärt, setzen wir daher einen Cache-Key im HMENU für die Menüs unterhalb der Hauptseiten (also die ersten mit special = list):

10 { 1.NO.stdWrap.cObject.20.cache { key { cObject = TEXT cObject { value.field = uid wrap = mainnav-lvl1-|_{siteLanguage:languageId} } insertData = 1 } } }

Cache: Level 2

Neu ist jetzt die Möglichkeit, dass wir dasselbe auch für die Ebene darunter machen können, weil dort jetzt auch HMENUs sind:

10 { 1.NO.stdWrap.cObject.20.2 { NO { stdWrap { cObject { 20 { cache { key { cObject = TEXT cObject { value.field = uid wrap = mainnav-lvl2-|_{siteLanguage:languageId} } insertData = 1 } } } } } } } }

Was ist mit dem aktivem Pfad?

Wer aufgepasst hat, dem ist nicht entgangen, dass wir bisher die Untermenüs rein anhand ihres Entrypoints cachen - ohne zu berücksichtigen, ob eine Seite im Menü aktiv ist.

Speichern wir uns also die Rootline der aktuellen Seite wieder in ein Register, und überschreiben falls nötig den Cache-Key. Nötig wird dies immer, falls der Entrypoint in der Rootline der aktuell aufgerufenen Seite liegt:

10 { 5 = LOAD_REGISTER 5.currentRootlinePids.cObject = HMENU 5.currentRootlinePids.cObject { special = rootline special.range = 0|-1 1 = TMENU 1 { NO { doNotLinkIt = 1 stdWrap.cObject = TEXT stdWrap.cObject.field = uid wrapItemAndSub = |, |*| |, |*| | } } } 10 { // Level 1 1.NO.stdWrap.cObject.20.cache { key.cObject.override { data = page:uid wrap = |_cur if { value.data = register:currentRootlinePids isInList.field = uid } } } // Level 2 1.NO.stdWrap.cObject.20.2 { NO { stdWrap.cObject.20.cache { key.cObject.override { data = page:uid wrap = |_cur if { value.data = register:currentRootlinePids isInList.field = uid } } } } } } }

95% weniger Datenbank-Transaktionen

Mit dieser TypoScript-Lösung könnten wir 95% der SQL-Queries einsparen - die einzige bessere Lösung aus unserem Vergleich (die aber auch Programmier-Knowhow voraussetzt) schaffte es mit viel PHP-Code auf 99% Einsparung.

Nochmals die Zahlen im Vergleich:

| Seite | Fluid-basiert | TS-basiert, 2 Caches | mit b13/menus (gepimpt) | |------------|---------------|----------------------|-------------------------| | Home | 6094 | 5206 | 7436 | | Impressum | 1259 | 56 | 18 | | Page 1 | 1251 | 116 | 11 | | Page 2 | 1251 | 113 | 11 | | Page 2.1 | 1250 | 116 | 11 |

TypoScript - mächtig, wie eh und je

TypoScript gibt es seit Ewigkeiten. Und, ja, nicht jedes Konstrukt darin ist eingängig. Für manches braucht es Erfahrung (und manchmal auch Schmerz), um es zu verstehen oder richtig anwenden zu können. An diesem Beispiel sieht man aber, dass TypoScript auch heute noch seine Daseinberechtigung (und gewisse Vorteile) haben kann - wenngleich es etwas sperriger erscheinen mag.

Der nicht zu unterschätzende Punkt ist: man braucht keine Programmierkenntnisse. Es ist doch fast schon genial, dass ein Integrator (der nach der TYPO3 Rollendefinition kein Programmierer ist) rein mit TypoScript das Menü so stark optimieren kann.

Nach fast 20 Jahren mit TypoScript bin ich immer wieder überrascht und begeistert, was mit etwas Konfiguration alles machbar ist. Daher bin ich definitiv im #teamTypoScript.

Zu gerne hätte ich diese Menü-Variante in unserem Menü-Vergleich ganz oben auf dem Siegertreppchen gesehen... :-(

Julian Hofmann

Julian

Julian Hofmann schaut sich Dinge gerne genauer an, will auch die Details verstehen - um sie dann bestmöglich in Projekten anzuwenden.

Backenddeveloper

Kennst du das: Immer nur schnell schnell?

Wie wäre es einmal mit Zeit und Respekt für Codequalität? Arbeiten im Team? Automatisierte Tests?

Komm zu uns

Unterrichtsausfall - und nun? Neues Portal für das Bayerische Staatsministerium für Unterricht und Kultus

Für das Bayerische Staatsministerium für Unterricht und Kultus durften wir eine neue WebApp bzw. ein neues Portal auf Basis eines bereits bestehenden, aber stark veralteten Portals entwickeln. Das...

Zum Beitrag

SQL: Zeige alle Tabellen absteigend nach Größe sortiert

Ich brauche in letzter Zeit häufiger den SQL-Befehl, um herauszufinden, welche Tabellen in der TYPO3-Datenbank am größten sind. Ich habe das Snippet einmal veröffentlicht.

Zum Beitrag

TYPO3 12 mit CKEditor 5: Stile als Einfachauswahl

Wenn man im RTE in TYPO3 einen Link setzt, kann es sein, dass man zwischen verschiedenen Link-Klassen auswählen muss, um beispielsweise Buttons im Frontend zu erzeugen. Neu ist in TYPO3 12 dass man...

Zum Beitrag

Null-Safe Operator im TYPO3-Bereich

Spätestens mit dem Einzug von PHP8 kann es an vielen Stellen zu Problemen mit undefinierten Arrays oder Variablen im Allgemeinen kommen. Hier ein paar Beispiele und einfache Lösung dafür.

Zum Beitrag

Die ersten/letzten Zeilen einer (SQL)-Datei löschen

Zu den nachfolgenden Befehlen gibt es eigentlich nicht viel zu sagen. Manchmal kann es nützlich sein, die ersten (bzw. letzten) X Zeilen aus einer Datei zu löschen. Und wenn die Datei zu groß zum...

Zum Beitrag

b13/container: Kindelemente in der Bearbeitungsansicht hinzufügen und ändern

Anders als in gridelements kann man in der Extension Container von B13 die Kindelemente nicht verwalten, wenn man den Container in der Bearbeitungsansicht öffnet. Wie man das schnell selber einbauen...

Zum Beitrag