Shopware

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.

Pixup MediaVeröffentlicht am 10 Min. Lesezeit

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:

Sitzung1.2.3.4.5.
leerer Warenkorb, kalter Cache56,63,43,43,53,3
ein Artikel im Warenkorb39,344,036,140,636,5
Gegenprobe, frische leere Sitzung3,22,62,92,62,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:

Sitzungsw-states1. Abruf2.3.4.5.
anonymkein Cookie412,6 ms5,23,42,92,9
angemeldetlogged-in61,7 ms48,346,673,153,5
anonym, Gegenprobekein Cookie4,4 ms3,02,92,72,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

PfadkaltwarmUrteil
/120,2 ms3,8 mswird gecacht
Kategorieseite59,7 ms3,7 mswird gecacht
Produktdetailseite63,4 ms3,6 mswird gecacht
/checkout/cart45,4 ms44,7 msnie
/account/login76,1 ms43,5 msnie

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.

Technische UnterstützungShopware-Agentur