Kamil Owczarek
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)?
├── NoUse SWR normally (all users get cached + background refresh)
└── YesIs user authenticated?
    ├── NoUse SWR (anonymous users get cached response)
    └── YesSkip 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.

EndpointZawiera dane o stanachZachowanie SWR
/productsTakPomija dla zalogowanych
/products/:idTakPomija dla zalogowanych
/landings/:nameNieZawsze SWR
/content/:slugNieZawsze SWR

Daje to kilka korzyści:

  1. Mniejsze wpisy w cache dla landing pages i stron treściowych
  2. Brak mylących danych o stanach na stronach, które mogą być nieaktualne
  3. 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:

  1. Twardo inwalidować — usunąć konkretne wpisy z cache
  2. 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:

  1. SWR jest potężne, ale uniwersalne cache'owanie dla wszystkich ma swoje ograniczenia
  2. Kontekst użytkownika (zalogowany kontra anonimowy) powinien wpływać na zachowanie cache
  3. Wrażliwość endpointu (dane o stanach kontra treść) też ma znaczenie
  4. 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.