- Opublikowano
Inteligentny cache SWR: świeżość danych zależna od kontekstu użytkownika w e-commerce
- Autorzy
Dylemat cache'owania w e-commerce
Każda platforma e-commerce mierzy się z tym samym fundamentalnym napięciem: szybkość kontra aktualność.
Cache sprawia, że strona jest szybka, ale dane w cache potrafią się zestarzeć. W większości przypadków serwowanie lekko nieaktualnej treści jest w porządku — opis produktu czy strona kategorii nie zmieniają się co minutę. Ale część danych jest wrażliwa na czas:
- Stany magazynowe — pokazanie "Dostępny", gdy produkt właśnie się wyprzedał, to fatalne doświadczenie dla klienta
- Ceny — ceny promocyjne muszą odzwierciedlać się natychmiast
- Dane specyficzne dla B2B — klienci biznesowi często mają wynegocjowane ceny albo zarezerwowany towar
Tradycyjne podejście jest binarne: albo cache'ujesz wszystko agresywnie (szybko, ale potencjalnie nieaktualnie), albo nie cache'ujesz niczego wrażliwego (aktualnie, ale wolno). Da się jednak mądrzej.
Jak działa Stale-While-Revalidate (SWR)
SWR to strategia cache'owania, która natychmiast serwuje nieświeżą treść, odświeżając cache w tle:
Request 1: Cache miss → Fetch fresh data → Store in cache → Return data
Request 2: Cache hit → Return cached data immediately
Request 3: Cache stale → Return cached data → Background refresh
Request 4: Cache hit → Return newly refreshed data
Kluczowa obserwacja jest taka, że użytkownicy dostają natychmiastowe odpowiedzi (świetny UX), a cache pozostaje rozsądnie świeży (dobra aktualność). "Okno nieświeżości" zależy od tego, jak często endpoint jest odpytywany.
Dla stron o dużym ruchu SWR działa znakomicie — cache odświeża się tak często, że nieaktualność jest minimalna. Dla stron o małym ruchu okno bywa dłuższe, ale zwykle jest to akceptowalne.
Problem: jedno podejście nie działa dla wszystkich
I tu robi się ciekawie. Rozważmy dwa typy użytkowników odwiedzających tę samą stronę produktu:
Anonimowy odwiedzający przeglądający produkty:
- Interesują go głównie informacje o produkcie, zdjęcia, opisy
- Aktualność stanu magazynowego ma znaczenie, ale kilka minut opóźnienia jest do zaakceptowania
- Szybkość jest kluczowa dla konwersji
Klient B2B składający duże zamówienie:
- Potrzebuje dokładnych stanów magazynowych do decyzji zakupowych
- Może mieć indywidualne ceny, które muszą być aktualne
- Podejmuje decyzje warte tysiące dolarów
- Może tolerować nieco dłuższe czasy ładowania w zamian za aktualność
Serwowanie obu tej samej odpowiedzi z cache nie ma sensu. Klient B2B potrzebuje świeżych danych, a anonimowy użytkownik szybkich.
Rozwiązanie: świeżość zależna od kontekstu
Pomysł jest prosty: pomijaj SWR dla zalogowanych użytkowników na endpointach wrażliwych na stany magazynowe.
Przepływ logiki wygląda tak:
Is this a stock-sensitive endpoint (products, inventory)?
├── No → Use SWR normally (all users get cached + background refresh)
└── Yes → Is user authenticated?
├── No → Use SWR (anonymous users get cached response)
└── Yes → Skip SWR (authenticated users always get fresh data)
Daje nam to najlepsze z obu światów:
- Anonimowi odwiedzający dostają maksymalną szybkość
- Zalogowani klienci B2B dostają maksymalną aktualność
- Cache nadal działa dla większości ruchu (odciążając bazę danych)
Wzorzec implementacji
Implementacja wymaga dwóch elementów: flagi w opcjach i sprawdzenia sesji.
type CacheOptions = {
/** When true, authenticated users always get fresh data */
requireFreshForLoggedIn?: boolean;
};
Handler cache sprawdza tę opcję w momencie decyzji o SWR:
// Simplified logic
if (cacheHit && !isStale) {
// Fresh cache - serve to everyone
return cachedData;
}
if (cacheHit && isStale) {
// Stale cache - decision point
const session = await getSession();
const skipSWR = options?.requireFreshForLoggedIn && session?.user;
if (skipSWR) {
// Authenticated user on sensitive endpoint
// Skip SWR, fetch fresh data
return await fetchFreshData();
}
// Anonymous user or non-sensitive endpoint
// Use SWR: return stale, refresh in background
backgroundRefresh();
return cachedData;
}
// Cache miss - fetch and cache
return await fetchFreshData();
Kluczowy warunek to:
!(options?.requireFreshForLoggedIn === true && session?.user)
Czyta się to tak: "pomiń SWR tylko wtedy, gdy OBA warunki są spełnione: endpoint wymaga świeżych danych dla zalogowanych ORAZ użytkownik jest zalogowany".
Stosowanie wzorca
Nie każdy endpoint potrzebuje takiego traktowania. Oto jak je pogrupować:
Endpointy wymagające świeżych danych dla zalogowanych:
- Strony szczegółów produktu (stany, ceny)
- Listingi produktów (filtry dostępności)
- API koszyka i checkoutu
- Zapytania o stany magazynowe
Endpointy, które mogą używać SWR dla wszystkich:
- Strony kategorii
- Treści z CMS-a
- Menu nawigacyjne
- Landing pages
- Statyczne treści marketingowe
Endpointy, których nigdy nie należy cache'ować:
- Dane profilu użytkownika
- Historia zamówień
- Przetwarzanie płatności
// Stock-sensitive endpoint
export default defineCachedHandler(async (event) => {
return await fetchProducts();
}, { requireFreshForLoggedIn: true });
// Non-sensitive endpoint - SWR for everyone
export default defineCachedHandler(async (event) => {
return await fetchLandingPage();
});
Rozdzielenie odpowiedzialności: jakie dane gdzie trafiają
Z tego wzorca wyszła dodatkowa optymalizacja: nie umieszczaj danych wrażliwych na czas w endpointach, które ich nie potrzebują.
Na przykład landing page może wyświetlać karty produktów. Pierwotnie w odpowiedzi landing page'a zwracaliśmy stany magazynowe i znaczniki świeżości. Ale landing pages korzystają z cache'owania SWR, więc te dane o stanach i tak mogły być nieaktualne.
Rozwiązanie: umieszczaj pola wrażliwe na stany magazynowe wyłącznie w endpointach, które pomijają SWR dla zalogowanych.
| Endpoint | Zawiera dane o stanach | Zachowanie SWR |
|---|---|---|
/products | Tak | Pomija dla zalogowanych |
/products/:id | Tak | Pomija dla zalogowanych |
/landings/:name | Nie | Zawsze SWR |
/content/:slug | Nie | Zawsze SWR |
Daje to kilka korzyści:
- Mniejsze wpisy w cache dla landing pages i stron treściowych
- Brak mylących danych o stanach na stronach, które mogą być nieaktualne
- Czysty podział odpowiedzialności
Strategia inwalidacji cache
Ten wzorzec współgra z inwalidacją cache. Gdy dane produktu się zmieniają (aktualizacja stanu, zmiana ceny), możesz:
- Twardo inwalidować — usunąć konkretne wpisy z cache
- Miękko inwalidować (trigger SWR) — oznaczyć wszystkie wpisy jako nieświeże, wyzwalając odświeżanie w tle
Trigger SWR jest łagodniejszy — nie powoduje lawiny zapytań do bazy. Zamiast tego każdy endpoint odświeża się leniwie przy kolejnym żądaniu.
// Soft invalidation - triggers SWR refreshes
await triggerSWRRevalidation();
// Hard invalidation - clears everything
await clearCacheCompletely();
Przy aktualizacjach stanów magazynowych używamy miękkiej inwalidacji:
- Anonimowi użytkownicy nadal dostają szybkie (lekko nieaktualne) odpowiedzi
- Zalogowani dostają świeże dane przy kolejnym żądaniu
- Cache zapełnia się stopniowo, bez przeciążania bazy danych
Mierzenie efektów
Metryki, które mają znaczenie:
Dla użytkowników anonimowych:
- Cache hit rate powinien pozostać wysoki (powyżej 90%)
- Czasy odpowiedzi powinny pozostać niskie (poniżej 100 ms dla cache)
- Brak wzrostu liczby zapytań do bazy
Dla użytkowników zalogowanych:
- Czasy odpowiedzi będą nieco wyższe (zapytania do bazy)
- Ale aktualność danych jest gwarantowana
- To akceptowalny kompromis dla procesów B2B
Ogólnie:
- Wolumen zapytań do bazy powinien spaść (większość ruchu jest anonimowa)
- Reklamacji dotyczących stanów magazynowych powinno ubyć (klienci B2B dostają świeże dane)
- Metryki szybkości strony powinny się poprawić (większość użytkowników dostaje odpowiedzi z cache)
Edge case'y i kwestie do rozważenia
Narzut na wykrywanie sesji: sprawdzanie autoryzacji dodaje opóźnienie. W naszym przypadku sprawdzenie sesji dzieje się po zaserwowaniu nieświeżego cache anonimowym użytkownikom, więc nie wpływa na ich doświadczenie.
Przepływy koszyka i checkoutu: te powinny całkowicie omijać cache, a nie tylko pomijać SWR. Wzorzec "requireFreshForLoggedIn" jest dla stron, które MOGĄ być cache'owane, a nie dla takich, których nigdy nie należy cache'ować.
Konsumenci API: jeśli udostępniasz API dla B2B (a nie tylko interfejs webowy), rozważ analogiczną logikę dla autoryzacji kluczem API, nie tylko dla sesji.
Cache na CDN: jeśli używasz CDN-a, musisz zadbać, żeby respektował nagłówki autoryzacyjne i nie cache'ował spersonalizowanych odpowiedzi dla anonimowych użytkowników.
Szerszy obraz
Ten wzorzec odzwierciedla szerszą zasadę: optymalizację świadomą kontekstu.
Zamiast traktować wszystkich użytkowników i wszystkie endpointy identycznie, analizujemy faktyczne wymagania:
- Kto wysyła żądanie?
- Jakich danych potrzebuje?
- Jak bardzo te dane są wrażliwe na nieaktualność?
- Jaki kompromis ma sens dla tej konkretnej kombinacji?
To samo myślenie stosuje się do:
- Jakości obrazów (serwuj niższą rozdzielczość na mobile przy wolnym łączu)
- Ziarnistości danych (wysyłaj mniej danych użytkownikom, którzy ich nie potrzebują)
- Dostępności funkcji (odkładaj niekrytyczne funkcje na wolnych urządzeniach)
Podsumowanie
Wzorzec "requireFreshForLoggedIn" rozwiązuje realny problem cache'owania w e-commerce: równoważy szybkość dla anonimowych odwiedzających z aktualnością dla zalogowanych klientów B2B.
Najważniejsze wnioski:
- SWR jest potężne, ale uniwersalne cache'owanie dla wszystkich ma swoje ograniczenia
- Kontekst użytkownika (zalogowany kontra anonimowy) powinien wpływać na zachowanie cache
- Wrażliwość endpointu (dane o stanach kontra treść) też ma znaczenie
- Oddzielenie danych wrażliwych na czas od cache'owanych endpointów to czysty wzorzec architektoniczny
Implementacja jest prosta — jedna flaga w opcjach i sprawdzenie sesji. Ale efekt jest znaczący: klienci B2B ufają danym, które widzą, a anonimowi odwiedzający cieszą się szybkim ładowaniem stron.
Czasem najlepsza strategia cache'owania nie polega na cache'owaniu więcej ani mniej — tylko na cache'owaniu mądrzej, w zależności od tego, kto pyta.