Ein Artikel im Warenkorb kostet den HTTP-Cache
Drei Millisekunden gegen vierzig. Sobald ein Kunde etwas in den Warenkorb legt, liefert Shopware ihm jede weitere Seite ungecacht aus — für die ganze Sitzung.
Ergebnis
3,4 ms gegen 40 ms
Bei leerer Sitzung wird eine Kategorieseite nach dem ersten Aufruf aus dem Cache bedient und antwortet in 3,3 bis 3,5 ms. Liegt ein einziger Artikel im Warenkorb, fällt kein Abruf mehr unter 36 ms — Faktor gut zwölf, und zwar für jede Seite dieser Sitzung, nicht nur für den Warenkorb.
„Der Shop ist langsam, sobald jemand etwas in den Warenkorb legt." Diesen Satz hört man in Projekten regelmäßig, und er klingt nach einem Gefühl. Er ist messbar.
Warum das in einer dev-Installation nicht zu messen ist
Ein früherer Anlauf blieb liegen, und der Grund gehört an den Anfang: In einer
dev-Runtime meldet jede Antwort denselben Zustand.
X-Symfony-Cache: GET /Food/Bakery-products/: miss, store
Fünf Abrufe hintereinander, mit Cookie-Jar, mit Browser-User-Agent — immer
miss. Der Cache legt ab und liefert nie aus. Ein Ergebnis aus diesem Zustand
wäre eine Eigenschaft des Labors gewesen, keine Aussage über das Produkt.
Die Lösung war kein Umschalten der bestehenden Testsysteme, sondern eine
eigene Runtime in prod. Ein Container, ein Einzeiler, danach wieder
entfernt.
Ein Hinweis für alle, die das nachstellen: In prod verschwindet der Header
X-Symfony-Cache — er ist ein Debug-Header. Der erste Ersatzindikator, den wir
probiert haben, war Age, und der war falsch. Age stieg auch auf
/checkout/cart monoton an, obwohl diese Seite no-store, private trägt. Dass
ausgerechnet der Warenkorb angeblich gecacht sein sollte, war der Verräter. Was
zählt, ist die Antwortzeit nach geleertem Cache-Pool.
Das Ergebnis
Kategorieseite, jeweils fünf Abrufe, Zeiten in Millisekunden:
| Sitzung | 1. | 2. | 3. | 4. | 5. |
|---|---|---|---|---|---|
| leerer Warenkorb, kalter Cache | 56,6 | 3,4 | 3,4 | 3,5 | 3,3 |
| ein Artikel im Warenkorb | 39,3 | 44,0 | 36,1 | 40,6 | 36,5 |
| Gegenprobe, frische leere Sitzung | 3,2 | 2,6 | 2,9 | 2,6 | 2,3 |
Die erste Zeile ist der Normalfall: Ein Abruf rendert, alle weiteren kommen aus dem Cache.
Die zweite Zeile ist der Befund: Kein einziger Abruf fällt unter 36 Millisekunden. Nicht der zweite, nicht der fünfte. Es gibt keinen Treffer, der verzögert einsetzt — es gibt gar keinen.
Die dritte Zeile ist die Gegenprobe und der Grund, warum die zweite belastbar ist. Eine frische, leere Sitzung bekommt sofort wieder Treffer, ohne dass der Cache neu aufgebaut werden müsste. Der Cache war die ganze Zeit warm. Er wurde nur nicht benutzt.
Der zweite Zustand: angemeldet
Dieselbe Seite, dieselbe prod-Runtime, eine Sitzung mit angelegtem und angemeldetem Kundenkonto:
| Sitzung | sw-states | 1. Abruf | 2. | 3. | 4. | 5. |
|---|---|---|---|---|---|---|
| anonym | kein Cookie | 412,6 ms | 5,2 | 3,4 | 2,9 | 2,9 |
| angemeldet | logged-in | 61,7 ms | 48,3 | 46,6 | 73,1 | 53,5 |
| anonym, Gegenprobe | kein Cookie | 4,4 ms | 3,0 | 2,9 | 2,7 | 2,8 |
Beide Zustände verhalten sich also gleich: kein einziger Treffer, solange der Zustand gesetzt ist, und sofort wieder Treffer, sobald eine frische anonyme Sitzung kommt.
Die Stelle, an der man es sehen kann
Der Schalter ist kein interner Zustand, sondern ein Cookie. Die anonyme Sitzung trägt es gar nicht, die angemeldete trägt:
sw-states = logged-in
sw-cache-hash = 93b8505807cacd95a6e15548c086b15a
Das ist praktisch die nützlichste Einzelbeobachtung dieser Messung: Wer wissen
will, ob eine Sitzung gecacht wird, muss nicht raten und nichts instrumentieren
— es genügt, im Browser nach sw-states zu sehen. Ist das Cookie da, ist der
Cache für diese Sitzung aus.
Dieselbe Mechanik trägt auch der vorgelagerte Reverse Proxy: sw-states ist
genau die Information, die ein Varnish oder ein CDN braucht, um dieselbe
Entscheidung zu treffen, ohne PHP zu fragen.
Die Ursache steht in einer Zeile Konfiguration
http_cache: ['logged-in', 'cart-filled']
Zwei Cache-Zustände. Trifft einer zu, wird die Seite für diese Sitzung nicht mehr aus dem Cache bedient. Das ist kein Fehler und keine Fehlkonfiguration — es ist die Voreinstellung, und sie hat einen guten Grund: Eine Seite kann den Warenkorb-Zähler im Kopfbereich enthalten, und ein Cache, der den falschen Zähler ausliefert, ist schlimmer als kein Cache.
Der Preis dafür steht in der Tabelle oben.
Was nie gecacht wird
| Pfad | kalt | warm | Urteil |
|---|---|---|---|
/ | 120,2 ms | 3,8 ms | wird gecacht |
| Kategorieseite | 59,7 ms | 3,7 ms | wird gecacht |
| Produktdetailseite | 63,4 ms | 3,6 ms | wird gecacht |
/checkout/cart | 45,4 ms | 44,7 ms | nie |
/account/login | 76,1 ms | 43,5 ms | nie |
Checkout und Kundenkonto tragen Cache-Control: no-store, private. Dass sie
nicht schneller werden, ist beabsichtigt und richtig.
Was das praktisch bedeutet
Die Kennzahl, die viele messen, ist die falsche. Ein Ladezeittest auf der Startseite misst den gecachten Pfad. Der Kunde, um den es geht, hat etwas im Warenkorb — und für den gilt die zweite Zeile der Tabelle. Wer Ladezeiten bewerten will, muss mit gefülltem Warenkorb messen.
Der Hebel liegt nicht im Cache, sondern darunter. Wenn ein Teil der Sitzungen ohnehin ungecacht läuft, entscheidet die Renderzeit. In dieser Messung sind das 36 bis 44 Millisekunden bei 18 Produkten. Ein echter Katalog liegt darüber, und genau dort zahlt sich Arbeit an Datenbank, Indizes und Template aus.
Es gibt eine Stellschraube, und sie ist eine Abwägung. Die Liste der
Cache-Zustände ist konfigurierbar. Wer cart-filled entfernt, kauft Geschwindigkeit
mit dem Risiko, dass ein Kunde im Kopfbereich einen fremden Warenkorb-Zähler
sieht — es sei denn, der Zähler wird ohnehin nachgeladen. Das ist eine
Entscheidung je Projekt, keine allgemeine Empfehlung.
Was wir nicht gemessen haben, obwohl wir es wollten
Shopware 6.7.13.1 kennt ein Feature-Flag namens CACHE_REWORK. Seine
Beschreibung im Kern spricht genau diesen Punkt an — Cache-Zustände entfernen
und Seiten auch für angemeldete Kunden oder bei gefülltem Warenkorb cachen.
Der erste Versuch schien zu zeigen, dass das Flag nichts ändert. Die Gegenprüfung ergab, dass 6.7.2.2 dieses Flag gar nicht kennt — null Fundstellen im Kern, während 6.7.13.1 es in fünfzehn Dateien trägt. Der Test war für diese Frage bedeutungslos, und das Ergebnis wäre eine erfundene Zahl gewesen.
Die Frage bleibt damit offen und braucht eine prod-Runtime auf 6.7.13.x. Sie steht auf der Liste.
So stellen Sie den Test nach
docker run -d --name sw-prod -p 127.0.0.1:8650:80 \
-e APP_ENV=prod -e APP_DEBUG=0 -e SHOPWARE_HTTP_CACHE_ENABLED=1 \
dockware/dev:6.7.2.2
# in der .env APP_ENV=prod setzen, dann:
php bin/console cache:clear
php bin/console cache:pool:clear cache.http
# fuenf Abrufe, leere Sitzung
for i in 1 2 3 4 5; do
curl -s -o /dev/null -w "%{time_total} " -b jar -c jar http://localhost/<kategorie>/
done
# Artikel in den Warenkorb, dann dieselben fuenf Abrufe mit demselben Jar
Entscheidend ist, dass der Cookie-Jar je Szenario getrennt bleibt. Wer denselben Jar weiterverwendet, misst zweimal dasselbe.
