Mega-Menü mit Cache optimieren

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!
Vergleich vorher-nachher mittels blackfire.io

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.

3 Comments

  1. 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“

    Reply
    • 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)

      Reply

Hinterlasse einen Kommentar.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.