Strategia renderowania storefront
Wybór strategii renderowania jest jedną z najważniejszych decyzji architektonicznych Twojego storefronta. Wpływa na:
- Latency widzianą przez kupującego (TTFB, LCP, Core Web Vitals → SEO ranking)
- Koszt infrastruktury (CF Workers CPU, R2 storage, opcjonalnie Durable Objects)
- Operacyjną złożoność (queue config, cache invalidation, debugging)
- Świeżość danych — czy zmiana ceny w admin jest widoczna natychmiast, w ciągu 5 minut, czy dopiero po redeploy
Ten przewodnik tłumaczy cztery realne strategie, ich trade-offy w głąb, profile shopów dla których każda ma sens, i rekomendacje DoSwiftly per profil.
Większość typowych merchantów dobrze działa na SSR + CF Edge cache (Model B niżej). ISR (Model C) jest performance optimization warta inwestycji dopiero gdy masz potwierdzony bottleneck. Pure SSR (Model A) jest OK dla małych sklepów lub gdy real-time freshness jest krytyczna i nie chcesz nawet 5-minutowego stale window.
Cztery rendering modes
| Model | Mechanizm | Cache scope | Świeżość |
|---|---|---|---|
| A. Pure SSR | Każdy request → fresh SSR → fresh GraphQL query | Brak cache | Real-time |
| B. SSR + CF Edge cache | SSR + Cache-Control: s-maxage=N → CF CDN cache'uje response | Per-PoP (per region CF) | s-maxage window (np. 5 min) |
| C. ISR (Next.js Incremental Static Regeneration) | Strony pre-rendered + cache w R2 + background revalidation przez queue | Globalny (R2 + regional) | revalidate: N window (np. 60s) + on-demand webhook |
| D. SSG + on-demand revalidate | Build-time generation + webhook → revalidatePath() | Static assets serve z CDN | Real-time po webhook |
Model A — Pure SSR (no cache)
Jak działa: Każdy request hituje Worker, Worker query'uje backend GraphQL, generuje HTML, zwraca. Zero cache layer między Worker a GraphQL response, zero Next.js runtime cache.
// app/products/[slug]/page.tsx
import { headers } from 'next/headers';
export default async function ProductPage({ params }: { params: { slug: string } }) {
await headers(); // ← czyni route dynamic (auto opt-out z static)
const product = await fetch(`${apiUrl}/graphql`, {
method: 'POST',
body: JSON.stringify({ query: PRODUCT_QUERY, variables: { slug: params.slug } }),
cache: 'no-store', // ← explicit bez Next.js cache
}).then((r) => r.json());
return <ProductLayout data={product.data.product} />;
}
Plus:
- Najprostsze do zrozumienia + debugowania
- Real-time freshness — admin zmienia cenę → następny request widzi nową cenę
- Zero CF infrastructure beyond Worker — nie wymaga R2 bucket, queue, Durable Objects
- Zero queue config = zero bug class typu "FatalError: Dummy queue is not implemented"
- Działa identycznie w hostingu współdzielonym (shared template) i dedykowanym (per-shop Worker)
Minus:
- TTFB ~200-500ms każdym requestem — cold SSR + GraphQL roundtrip. ISR cache hit to 10-50ms.
- Backend GraphQL pressure proporcjonalna do traffic — 1000 RPS = 1000 GraphQL queries/s
- Worker CPU 1101 risk — bursty traffic (sale day, marketing campaign) może wysycić 30s CPU budget Workers Paid tier
- LCP nieoptymalne — wpływa na Core Web Vitals → SEO ranking lower vs cached models
Model B — SSR + CF Edge cache (HTTP layer)
Jak działa: SSR jak Model A, ale response ma Cache-Control: public, s-maxage=N header. Cloudflare CDN automatycznie cache'uje response po pierwszym hicie w danym PoP. Kolejne requesty z tego PoP serwują z cache bez Worker invocation.
// middleware.ts (lub proxy.ts dla Next.js 16)
import { NextResponse } from 'next/server';
export function middleware(request: Request) {
const response = NextResponse.next();
// CF Edge cache 5 min, stale-while-revalidate 60s
response.headers.set(
'Cache-Control',
'public, s-maxage=300, stale-while-revalidate=60',
);
return response;
}
export const config = {
matcher: ['/products/:path*', '/categories/:path*', '/blog/:path*'],
};
Plus:
- Performance prawie ISR-grade dla powtarzającego się traffic — po pierwszym hicie w PoP, kolejne ~10ms
- Zero Next.js cache complexity — żaden
revalidate, queue, R2 bucket, DOQueueHandler. Jeden header w middleware. - Predictable cost — CF Edge cache jest "free" (zliczany jako bandwidth, ale Worker invocation oszczędzony)
- Worker CPU drastically reduced — typical cache hit ratio 80-95% (po warm-up) → Worker uruchamia się tylko na 5-20% requestów
Minus:
- Per-PoP cache zamiast globalnego — Cloudflare ma ~310 PoP'ów. Pierwszy request w nowym PoP zawsze cold. Dla shopa z globalnym traffic (US + EU + APAC) cache warmth jest fragmentowana.
- Time-based invalidation only — admin save ceny → nie ma instant invalidation. Trzeba czekać
s-maxagewindow. Workaround: CF Cache Purge API call po admin save (extra integration). - Brak per-route fine tuning — wszystko przez generic middleware. ISR daje
export const revalidate = Nper route z różnymi window'ami. - Stale-while-revalidate ograniczone — CF respektuje
stale-while-revalidateale tylko per region (jeśli kontrolujesz Worker side też). - Odpowiedzi z
Set-Cookienie są cache'owane przez CF Edge — strony ustawiające cookie koszyka (cart-id), waluty lub języka renderują się dynamicznie, omijając cache (patrz ostrzeżenie przy Modelu C niżej).
Model C — ISR (Next.js Incremental Static Regeneration)
Jak działa: Strony są pre-rendered i cache'd w R2 bucket. Każdy request serwuje z cache. Po revalidate window mija, background worker odświeża cache asynchronously (queue + worker self-invoke przez WORKER_SELF_REFERENCE service binding lub Durable Object based queue).
// app/products/[slug]/page.tsx
export const revalidate = 60; // cache 60s, background revalidate after
export default async function ProductPage({ params }: { params: { slug: string } }) {
const product = await fetch(`${apiUrl}/graphql`, {
method: 'POST',
body: JSON.stringify({ query: PRODUCT_QUERY, variables: { slug: params.slug } }),
next: { revalidate: 60, tags: [`product-${params.slug}`] }, // tag-based invalidation
}).then((r) => r.json());
return <ProductLayout data={product.data.product} />;
}
// On-demand invalidation (np. webhook handler)
import { revalidateTag } from 'next/cache';
await revalidateTag(`product-${productSlug}`); // ← instant flush specific cache
Plus:
- Najlepsze TTFB — cache hit 10-50ms, globally consistent (R2 jest globalna)
- Per-route tunable —
revalidate: 60na produktach,revalidate: 3600na blog'u,revalidate: 86400na statycznych stronach - On-demand invalidation — webhook po admin save →
revalidateTag()→ instant flush specific page - Worker CPU dramatically reduced — większość requestów to cache reads z R2, brak SSR + GraphQL
- Battle-tested OpenNext pattern — standard dla CF Workers Next.js storefronts
Minus:
- Complexity — queue handler (memoryQueue lub doQueue), R2 bucket per shop, cache interception layer
- Cost — R2 storage ($0.015/GB-month) + Class A/B ops + (opcjonalnie) Durable Objects requests $0.15/M
- Debugging difficulty — gdy revalidation fail'uje, log w
IgnorableError(MemoryQueue) lub orphaned DO state (doQueue). Stale content bez signal. - Stale window inherent — między
revalidateexpiry i next user visit, ktoś widzi stary state (np. cena bumped admin, ale pierwszy view dostaje stale 60s window) - Migration debt — doQueue wymaga
[[migrations]]znew_sqlite_classes. Zmiana DO class definition = breaking migration. Hard to undo w produkcji.
Storefront DoSwiftly ustawia first-party cookies (cart-id, waluta, język). Każda odpowiedź z nagłówkiem Set-Cookie jest wykluczona z pełnostronicowego cache — zarówno z CF Edge cache (Model B), jak i z pełnostronicowego ISR / SSG (Model C / D). Takie strony renderują się dynamicznie per request, niezależnie od export const revalidate. W praktyce dotyczy to większości stron z koszykiem, przełącznikiem waluty/języka lub stanem sesji — pełnostronicowy ISR działa najlepiej dla stron bez personalizacji per-cookie (statyczne strony treści, listingi bez stanu sesji).
Ważne dla kosztów: pełnostronicowy cache może być pomijany, ale data-cache (fetch() z next: { revalidate, tags }) wciąż zapisuje i czyta R2 — także na stronach renderowanych dynamicznie, bo cache fetch jest niezależny od dynamiczności strony. Operacje zapisu R2 z aktywnego data-cache są realnym, rosnącym kosztem (patrz sekcja Cost model niżej). Innymi słowy: "cache R2 się zapełnia" nie znaczy "pełnostronicowy ISR działa" — to mogą być wyłącznie wpisy data-cache.
Multi-currency: jeśli storefront obsługuje wiele walut przez cookie, strona wyrenderowana dla jednej waluty nie może być serwowana z pełnostronicowego cache dla innej. To kolejny powód, dla którego strony cenowe pozostają dynamiczne.
Model D — SSG + on-demand revalidate
Jak działa: Build-time generuje wszystkie product pages do static HTML (generateStaticParams). Static assets serwowane bezpośrednio z CF CDN. Admin save → webhook → revalidatePath('/products/[slug]') API call → re-render that one page.
// app/products/[slug]/page.tsx
export async function generateStaticParams() {
const products = await fetch(`${apiUrl}/graphql`, {
method: 'POST',
body: JSON.stringify({ query: ALL_PRODUCT_SLUGS_QUERY }),
}).then((r) => r.json());
return products.data.products.map((p) => ({ slug: p.slug }));
}
export const dynamicParams = false; // 404 dla nie-prebuilt slugs
export default async function ProductPage({ params }: { params: { slug: string } }) {
// ... same as ISR
}
Plus:
- Najszybsze latency — 5-20ms (static asset z CDN, zero Worker invocation)
- Maksymalna SEO performance — pre-rendered HTML, idealny LCP
- Predictable cost — zero per-request Worker CPU, tylko CDN bandwidth
- Real-time admin updates — webhook trigger natychmiastowy invalidation specific page
Minus:
- Build time scales with catalog — 1000 produktów = 1000 generation steps przy każdym
doswiftly deploy. Catalog z 10k+ SKU = 30+ minut build. - Big inventory changes require full re-deploy — dodanie 100 produktów = 100 nowych pages do prebuild
- Operational complexity — webhook → backend → CF Worker
/api/revalidateendpoint. Wymaga reliable webhook delivery + dedup logic. - Cold start dla nie-przebuilt routes — jeśli
dynamicParams: true, nowo dodany produkt nie ma prebuilt page → fall back do SSR za pierwszym razem - Rzadko stosowane w e-commerce — większość pattern jest hybrid (ISR dla product pages + SSG dla static content). Pure SSG działa dla shopów z stabilnym catalog.
Request flow — gdzie wchodzi cache
Kluczowe obserwacje z diagramu:
- Model A (Pure SSR) — Worker uruchamia się przy KAŻDYM request. Backend GraphQL hit zawsze.
- Model B (Edge cache) — większość requestów kończy się na CF Edge (zielona ścieżka). Worker invocation tylko cache miss. Backend hit tylko cache miss.
- Model C (ISR) — cache hit kończy się na R2 read (najszybsze ze wszystkich cached responses). Background regen przez queue + self-invoke happens asynchronously (user dostaje stale cached response, fresh regen pojawia się dla następnego user'a).
- Model D (SSG) — najkrótsza ścieżka: CDN serve static asset, Worker nigdy nie uruchomiony (dopóki webhook revalidate flow nie active).
Trade-offy w głąb
1. Latency profile (TTFB)
| Model | Cold cache | Warm cache | Worst case |
|---|---|---|---|
| A. Pure SSR | 200-500ms | n/a | Backend GraphQL slowdown |
| B. SSR + Edge cache | 200-500ms (first PoP hit) | 10-30ms (CF Edge) | Cache eviction + global PoP fragmentation |
| C. ISR | 200-500ms (cold regen) | 10-50ms (R2 + regional) | Queue saturation, R2 throttling |
| D. SSG | 5-20ms (CDN serve) | 5-20ms (static) | Cache eviction → fall to ISR/SSR |
Impact na Core Web Vitals (LCP):
- Pure SSR: LCP zazwyczaj 1.5-2.5s (above "Good" threshold). Google ranking impact: noticeable.
- Edge cache / ISR / SSG: LCP zazwyczaj 0.8-1.5s. Tier "Good", boost SEO ranking.
2. Backend GraphQL load
| Model | Backend hits / request | Backend hits / minute (przy 1000 RPM) |
|---|---|---|
| A. Pure SSR | 1.0 (każdy request) | 1000 |
| B. Edge cache (s-maxage=300, 80% hit ratio) | ~0.2 | 200 |
| C. ISR (revalidate=60, 95% cache hit) | ~0.05 | 50 |
| D. SSG (rare webhook) | ~0 (poza initial build + webhooks) | <10 |
Implication: bardzo wysokorouchowy sklep na Pure SSR może być backend GraphQL bottleneck zanim CF Worker zostanie nasycony. ISR daje 20× redukcję backend load — istotne przy ramping.
3. Worker CPU profile
CF Workers Paid plan: 30 sekund CPU budget per request, 128 MB memory, 10,000 sub-requests per invocation. Każda SSR routes:
- HTML rendering: ~50-200ms CPU
- GraphQL fetch + JSON parsing: ~50-200ms CPU
- Layout + child components: ~50-100ms CPU
- Total: 150-500ms CPU per request
Przy bursty traffic (np. 50 RPS cold) i bez cache, Worker invocation overlap może wysycić 30s budget → Error 1101. To dokładnie incydent GameGoods 2026-05-18 przed Commit 1.
| Model | Worker CPU per request (avg) | 1101 risk profile |
|---|---|---|
| A. Pure SSR | Full SSR cost (150-500ms) | HIGH przy bursty traffic |
| B. Edge cache | Full koszt cold, ~5ms warm (cache hit) | LOW po warm-up |
| C. ISR | Full koszt cold regen, ~5ms cache hit | LOW (z R2 cache) |
| D. SSG | ~0 (CDN serves static) | NEGLIGIBLE |
4. Cost model (DoSwiftly platform — Workers Paid $5/mo)
| Model | R2 cost | DO cost | Backend hit cost | Net |
|---|---|---|---|---|
| A. Pure SSR | 0 | 0 | Wysoki (każdy request) | Tani infrastrukturalnie, drogi operacyjnie (backend scaling) |
| B. Edge cache | 0 | 0 | Niski (5-20% hit ratio) | Najlepszy cost/performance ratio dla typowych shopów |
| C. ISR z MemoryQueue | R2 storage + Class A/B ops | 0 | Niski | Średni — R2 cost rośnie z catalog size |
| C+. ISR z doQueue | R2 + DO storage + DO ops | DO requests $0.15/M | Niski | Wysoki dla high-traffic — DO ops dominate przy ramping |
| D. SSG | 0 (assets CDN) | 0 | Bardzo niski (per webhook) | Najtańszy operacyjnie, ale build time cost |
DoSwiftly platform aspect: R2 free tier = 10 GB storage + 1M Class A ops + 10M Class B ops / m-c. Storage skaluje się tanio (~100 shopów × ~10MB ≈ 1 GB, ~10% free tier), ale to operacje Class A (zapisy data-cache) wyczerpują free tier znacznie wcześniej — już ~jeden aktywny sklep zbliża się do limitu 1 mln Class A/m-c (patrz ostrzeżenie przy Modelu C). Nie storage jest tu ograniczeniem skali, lecz liczba operacji zapisu.
5. Świeżość danych — kiedy admin save jest widoczna
| Model | Time-to-fresh dla returning visitor | Time-to-fresh dla new visitor |
|---|---|---|
| A. Pure SSR | Natychmiast (next request) | Natychmiast |
| B. Edge cache (s-maxage=300) | Po 5 min lub po CF cache purge API | Pierwszy request w PoP = fresh |
| C. ISR (revalidate=60) | Po 60s + 1 więcej request (background regen) lub webhook | Pierwszy request after cache miss |
| D. SSG + webhook | Natychmiast po webhook (od admin save do CDN propagation: ~5s) | Natychmiast |
Use case implication: jeśli Twój sklep ma stałe okazje promocyjne minute-by-minute (flash sales, real-time stock display), Pure SSR lub SSG+webhook są najlepsze. ISR revalidate: 60 może mieć 60s window gdzie cena/stock są stale.
Niezależnie od wybranego modelu, platforma automatycznie utrzymuje statyczne pliki kilku poprzednich wersji Twojego storefronta przez kolejne wdrożenia. Dzięki temu po doswiftly deploy stary HTML, który wciąż jest serwowany z cache (Edge lub ISR, w ramach okna stale-while-revalidate), nadal odwołuje się do istniejących zasobów — nie trafia na brakujące pliki _next/static/… i nie wywołuje pętli twardych przeładowań strony. Gdy okno cache wygaśnie, odwiedzający płynnie otrzymują nową wersję.
6. Operational complexity & failure modes
| Model | Setup | Debug | Failure mode | Recovery |
|---|---|---|---|---|
| A | None | Browser DevTools, backend logs | Backend GraphQL outage → 500 dla użytkownika | Auto po backend recovery |
| B | Middleware Cache-Control headers | CF Edge cache visible w CF dashboard | Stale content po cache key bump bez purge | CF Cache Purge API or wait s-maxage window |
| C | OpenNext queue + R2 binding + cache interception | OpenNext debugCache + Worker Logs | Queue failure → log IgnorableError, stale content niezauważone | Per-page revalidatePath() manual flush |
| C+ doQueue | OpenNext queue + R2 + Durable Object class + migration tag | DO state debugging trudny | DO migration drift → deploy fail, lub orphaned DO state | Manual CF dashboard DO inspection + force migration tag bump |
| D | generateStaticParams + webhook handler | Build logs, webhook delivery logs | Webhook delivery failure → stale content do next build/deploy | Re-trigger webhook lub manual revalidatePath() call |
7. SEO impact
Wszystkie cztery modele serwują pełen pre-rendered HTML (Google crawler dostaje co potrzebuje). Różnica głównie w Core Web Vitals scoring:
- LCP (Largest Contentful Paint): SSG/ISR/Edge cache: 0.8-1.5s (Good). Pure SSR: 1.5-2.5s (Needs Improvement).
- TTFB: SSG: 5-20ms. Edge cache/ISR: 10-50ms. Pure SSR: 200-500ms.
- Wpływ na ranking: Google Search Console traktuje Core Web Vitals jako ranking factor od 2021. Sklepy z LCP > 2.5s mogą mieć obniżoną pozycję dla competitive keywords.
Profile shopów — kiedy która strategia
Legenda kolorów: 🟢 rekomendowany default · 🟡 prosty model dla niskiej skali · 🔵 zaawansowany model dla performance-critical scenariuszy.
Mały sklep (< 50 RPS, ~100 SKU, hobby/side project)
Charakterystyka: traffic stable bez bursty patterns, mały catalog, admin updates kilka razy dziennie.
Rekomendacja: Pure SSR (Model A)
- Operacyjna prostota wygrywa nad latency optimization
- Backend pressure niska, brak ryzyka 1101
- Brak ISR complexity do debugowania gdy coś nie działa
- Real-time freshness za darmo (cena zmieniona w admin → widoczna od następnego request)
Alternatywa: SSR + Edge cache (Model B) jeśli już masz wiele PoP traffic i chcesz lepszy LCP — ale dla małego sklepu różnica może być nieistotna.
Średni sklep (50-500 RPS, ~1000 SKU, regularny biznes)
Charakterystyka: stabilny traffic z okazjonalnymi peakami (marketing campaigns), catalog updates kilka razy w tygodniu, freshness ważna ale 5-minutowy stale window akceptowalny.
Rekomendacja: SSR + CF Edge cache (Model B)
- Najlepszy cost/performance ratio
- Zero ISR complexity, ale 80%+ cache hit ratio = drastycznie zredukowany backend load
- Worker CPU 1101 risk drastycznie zredukowany (warm cache ratio)
- 5-min
s-maxagewindow jest acceptable dla większości scenariuszy (cena zmieniona → widoczna w 5 min, bez explicit cache purge) - Operacyjnie prosty: middleware setting Cache-Control, koniec
Alternatywa: ISR (Model C) jeśli wymaga się natychmiastowej invalidacji po admin save (webhook → revalidateTag).
Duży sklep (500+ RPS, 10000+ SKU, performance-critical)
Charakterystyka: high traffic z burst patterns (sale events, marketing TV ad spike), catalog często updated (multiple times daily), competitive market gdzie LCP matters dla SEO.
Rekomendacja: ISR z webhook revalidation (Model C + on-demand)
- 95%+ cache hit ratio → backend GraphQL pressure manageable
- Per-route fine-tuning (
revalidate: 60na hot products,revalidate: 3600na statycznych stronach) - On-demand
revalidateTag()po admin save → instant invalidation specific products - LCP <1s achievable
Queue choice:
- MemoryQueue (default po Commit 5) dla shopów do ~500 RPS sustained
- doQueue (opt-in, jeśli kiedyś dostępne) dla bursty patterns >500 RPS — cross-isolate dedup + retry
Marketplace / wysokorouchowe shopy z volatile catalog
Charakterystyka: bardzo duży catalog (50k+ SKU), inventory volatile, traffic distribution długi ogon (każdy produkt może być viewed).
Rekomendacja: Hybrid SSG + ISR fallback
- Top 1000 najpopularniejszych produktów: SSG (Model D) — pre-built w build, fast static
- Long-tail products: ISR fallback (
dynamicParams: true) — generated on-demand z cache - Webhook revalidation dla inventory updates
To jest najbardziej zaawansowany model. Tylko dla shopów które uzasadniają inżynierski effort.
DoSwiftly platform considerations
CF Workers Paid plan ($5/mo) — limity ważne dla wyboru
| Limit | Wartość | Impact na model |
|---|---|---|
| Workers requests / month | 10M | Model A/B mocno limited przez backend load. ISR oszczędza Worker invocations. |
| CPU time per request | 30s (Bundled) | Pure SSR ryzyko 1101 przy burst |
| Sub-requests per invocation | 10,000 | All models OK |
| R2 free tier | 10 GB + 1M Class A + 10M Class B / m-c | ISR storage cost rośnie z catalog size |
| Durable Objects | Paid extra ($0.15/M requests + storage) | doQueue (advanced ISR) ma extra koszt |
R2 free tier model cost projection
Per shop, ISR z R2 cache:
- ~100 produktów × ~50KB rendered HTML = ~5 MB stored
- ~1000 produktów × ~50KB = ~50 MB stored
- Class A (write) ops: revalidacje + zapisy data-cache (
fetchzrevalidate/tagami) = dziesiątki tysięcy/dobę dla aktywnego sklepu (rzędu ~1 mln/m-c) — data-cache zapisuje do R2 także na stronach renderowanych dynamicznie (per-cookie) - Class B (read) ops: odczyty cache = setki tysięcy/dobę dla aktywnego sklepu (rzędu kilku mln/m-c)
Już jeden aktywny sklep potrafi wyczerpać darmowy limit 1 mln operacji Class A (zapisy data-cache). To operacje Class A — nie storage ($0,015/GB jest tani) — są dominującym kosztem marginalnym przy skalowaniu.
Implikacje:
- Storage rośnie wolno (~50 MB/sklep) — daleko od limitu 10 GB
- Operacje Class A (zapisy) przekraczają free tier najszybciej — przy wielu aktywnych sklepach to one generują rachunek R2
- R2 paid tier: Class A $4,50/mln (zapisy/listy), Class B $0,36/mln (odczyty), storage $0,015/GB-mies, egress $0
Implikacje hostingu współdzielonego (multi-tenant)
Storefronty na hostingu współdzielonym (shared template Worker dla N tenantów) mają specyficzny problem z ISR:
OpenNext default MessageGroupId dla revalidation queue jest per-route (np. /products/abc). W dedykowanym Worker (single-tenant) to OK — jeden tenant, jeden product slug, jeden DO/Memory dedup key.
W hostingu współdzielonym (multi-tenant):
- Tenant A (
acme.doswiftly.pl) + Tenant B (widgets.doswiftly.pl) oba mają route/products/abc(oba mają produkt z tym slug, ale różne treści) - Default
MessageGroupId = "/products/abc"→ same dedup key dla obu tenantów - Revalidation jednego może być zignorowane przez dedup z drugim
- Effective: cross-tenant data leak via cache (rzadkie ale możliwe)
Wizualizacja problemu:
Effective failure: Tenant B revalidation jest ignored przez dedup. Tenant B widzi stale content możliwie z Tenant A render (cross-tenant data leak via cache).
Rozwiązania:
- Hosting współdzielony = Model A lub B (no Next.js ISR) — multi-tenant collision risk znika
- Custom queue wrapper z tenant-scoped
MessageGroupId = ${hostname}_${route}— wymaga custom JS code wopen-next.config.ts - Force
dynamic = 'force-dynamic'wszystkie routes w hostingu współdzielonym — efektywnie Model A
DoSwiftly rekomenduje Model A/B dla hostingu współdzielonego templates, ISR (Model C) dla dedykowanego hostingu (per-shop dedicated Worker = single-tenant scope = brak collision risk).
Decision matrix — quick reference
Wybór per zmienna:
| Zmienna | Pure SSR | Edge cache | ISR | SSG+webhook |
|---|---|---|---|---|
| Catalog size | dowolny | dowolny | mały-średni | mały-średni (build time) |
| Traffic | < 50 RPS | 50-1000 RPS | 100-10000+ RPS | dowolny |
| Update frequency | dowolny | godzinowy+ | godzinowy/dzienny | godzinowy+ (webhook trigger) |
| Real-time freshness wymóg | TAK | NIE (5 min OK) | NIE (60s OK) | TAK (webhook) |
| Operacyjna złożoność tolerancja | NISKA | NISKA | ŚREDNIA | WYSOKA |
| LCP target | < 2.5s | < 1.5s | < 1s | < 0.5s |
| Backend GraphQL capacity | wysoka | średnia | niska | minimal |
| Hosting współdzielony (multi-tenant) | ✅ safe | ✅ safe | ⚠️ collision risk | ⚠️ build-time burden |
| Dedykowany (single-tenant) | ✅ | ✅ | ✅ ideal | ✅ niche |
Migration paths między modes
Możesz przejść między modes bez przerwania serwisu:
A → B (Pure SSR → Edge cache)
- Dodaj
middleware.tszCache-Control: s-maxage=300dla wybranych routes - Deploy
- Cache się rozgrzewa automatically po requestach
Czas: ~10 min implementation, ~5 min deploy
B → C (Edge cache → ISR)
- Usuń
middleware.tscache headers (uniknij konfliktu) - Dodaj
export const revalidate = Nper route - Verify
open-next.config.tsmar2IncrementalCache + queue: memoryQueue + enableCacheInterception(CLI ≥1.1.2 default) doswiftly deploy— backend auto-provisionuje R2 bucket per shop- Pierwszy request każdej strony = cold regen, kolejne z cache
Czas: ~30 min implementation, ~5 min deploy, ~24h cache warm-up dla wszystkich routes
C → D (ISR → SSG)
- Dodaj
generateStaticParamsper route - Set
export const dynamicParams = false(lubtruejeśli chcesz fallback) - Add webhook handler dla
revalidatePath()calls - Backend webhook integration (DoSwiftly admin → Twój webhook endpoint po admin save)
doswiftly deploy— build time wzrasta proporcjonalnie do catalog
Czas: ~2-4h implementation (webhook integration najmniej trywialny), ~5-30 min deploy zależnie od catalog size
Odwrót (D → C → B → A)
Każdy krok jest reversible — można cofnąć dowolnie. Najprościej z A do D (każdy step adds layer), trochę trudniej w drugą stronę (cleanup poprzedniej warstwy).
Recommended defaults (DoSwiftly)
Standard merchant (większość przypadków):
- Model B (SSR + CF Edge cache) jako default starting point
- Operacyjna prostota + dobry performance dla typowego shopa
- Łatwy upgrade do Model C gdy potrzeba on-demand invalidation
Power merchant (>500 RPS, performance-critical):
- Model C (ISR) z webhook revalidation
- CLI deploy z default
queue: memoryQueue(CLI ≥1.1.2) automatically wired - Per-route
revalidatetuning per profile content (60s products, 3600s blog, etc.)
Hobby/MVP shop (< 10 RPS):
- Model A (Pure SSR) jest OK — nie warto inwestować w cache layer dla niskiego traffic'u
- Upgrade do Model B gdy traffic ramping (próg ~50 RPS)
Hosting współdzielony (shared templates):
- Model A lub B (rekomendowane B z Edge cache)
- ISR (Model C) ma multi-tenant collision risk — wymaga custom queue wrapper, nie rekomendowane bez deep audit
Projektowanie tras katalogu — routing-by-type
Struktura URL-i katalogu to decyzja niezależna od modelu renderowania, ale
silnie wpływa na obciążenie backendu (sekcja Backend GraphQL load wyżej) i CPU
Workera. Rekomendacja: routing-by-type — osobny prefiks trasy per typ zasobu,
tak jak w przykładach w tym przewodniku (/products/:path*, /categories/:path*):
/products/[handle]— strona produktu/categories/[handle]— strona kategorii/collections/[handle],/brands/[handle]— kolekcje, marki
Frontend zna typ zasobu z prefiksu trasy, więc pobiera dokładnie jedno,
właściwe zapytanie dla tego typu. Śmieciowy URL, który nie pasuje do żadnego
prefiksu (np. /backup.sql), kończy się 404 storefrontu bez ani jednego
wywołania API.
/[handle] ze spekulatywnym fan-outemTrasa jednosegmentowa, w której jeden segment może być produktem, kategorią, kolekcją lub marką, nie zna z góry typu zasobu. Zwykle rozwiązuje to spekulatywnym fan-outem — odpala kilka zapytań naraz (spróbuj jako produkt, jako kategoria, jako kolekcja…) i bierze pierwsze trafienie. Dwa koszty:
- Amplifikacja — jeden request = N zapytań do API zamiast jednego.
- Łapie każdy śmieć — trasa dopasowuje dowolny segment, w tym ruch
skanerów podatności (
/backup.sql,/.env,/.git/config). Każdy taki URL wymusza komplet spekulatywnych zapytań, zanim okaże się, że to nic.
Routing-by-type strukturalnie eliminuje oba koszty: junk nie pasuje do żadnego prefiksu → 404 bez zapytania.
Gdy single-segment jest konieczny (UX / SEO)
Krótkie, jednosegmentowe URL-e bywają wymogiem (SEO, czytelność linku). Wtedy dodaj junk-guard — tanią, lokalną regułę odrzucającą oczywiście nie-handle'owe segmenty przed jakimkolwiek zapytaniem:
import { notFound } from 'next/navigation';
// Odrzuć segment, zanim cokolwiek pobierzesz z API.
function isLikelyHandle(segment: string): boolean {
// Rozszerzenie pliku = ruch skanera, nie handle (.sql, .env, .php, .zip…).
if (/\.[a-z0-9]{1,8}$/i.test(segment)) return false;
// (opcjonalnie) wymuś gramatykę slug — dostrój do formatu handle w swoim sklepie.
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(segment)) return false;
return true;
}
// app/[handle]/page.tsx
export default async function CatalogEntryPage({
params,
}: {
params: Promise<{ handle: string }>;
}) {
const { handle } = await params; // params to Promise — zawsze await
if (!isLikelyHandle(handle)) notFound(); // 404 bez zapytania do API
// ... dopiero teraz rozwiązanie handle → zasób
}
Guard jest non-breaking dla realnych URL-i (prawdziwe handle przechodzą) i ścina większość ruchu skanerów, zanim dotknie API. To jednak łata objawu — docelowo routing-by-type jest tańszy i czystszy.
Następne kroki
- CLI commands —
doswiftly deployopcje, w tymdeploy.openNextCachedla opt-out - Storefront SDK API — GraphQL queries, fragments, mutations