Przejdź do głównej zawartości

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.

TL;DR

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

ModelMechanizmCache scopeŚwieżość
A. Pure SSRKażdy request → fresh SSR → fresh GraphQL queryBrak cacheReal-time
B. SSR + CF Edge cacheSSR + Cache-Control: s-maxage=N → CF CDN cache'uje responsePer-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 queueGlobalny (R2 + regional)revalidate: N window (np. 60s) + on-demand webhook
D. SSG + on-demand revalidateBuild-time generation + webhook → revalidatePath()Static assets serve z CDNReal-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-maxage window. 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 = N per route z różnymi window'ami.
  • Stale-while-revalidate ograniczone — CF respektuje stale-while-revalidate ale tylko per region (jeśli kontrolujesz Worker side też).
  • Odpowiedzi z Set-Cookie nie 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 tunablerevalidate: 60 na produktach, revalidate: 3600 na blog'u, revalidate: 86400 na 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 revalidate expiry i next user visit, ktoś widzi stary state (np. cena bumped admin, ale pierwszy view dostaje stale 60s window)
  • Migration debt — doQueue wymaga [[migrations]] z new_sqlite_classes. Zmiana DO class definition = breaking migration. Hard to undo w produkcji.
Strony personalizowane per-cookie omijają pełnostronicowy cache

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/revalidate endpoint. 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)

ModelCold cacheWarm cacheWorst case
A. Pure SSR200-500msn/aBackend GraphQL slowdown
B. SSR + Edge cache200-500ms (first PoP hit)10-30ms (CF Edge)Cache eviction + global PoP fragmentation
C. ISR200-500ms (cold regen)10-50ms (R2 + regional)Queue saturation, R2 throttling
D. SSG5-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

ModelBackend hits / requestBackend hits / minute (przy 1000 RPM)
A. Pure SSR1.0 (każdy request)1000
B. Edge cache (s-maxage=300, 80% hit ratio)~0.2200
C. ISR (revalidate=60, 95% cache hit)~0.0550
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.

ModelWorker CPU per request (avg)1101 risk profile
A. Pure SSRFull SSR cost (150-500ms)HIGH przy bursty traffic
B. Edge cacheFull koszt cold, ~5ms warm (cache hit)LOW po warm-up
C. ISRFull koszt cold regen, ~5ms cache hitLOW (z R2 cache)
D. SSG~0 (CDN serves static)NEGLIGIBLE

4. Cost model (DoSwiftly platform — Workers Paid $5/mo)

ModelR2 costDO costBackend hit costNet
A. Pure SSR00Wysoki (każdy request)Tani infrastrukturalnie, drogi operacyjnie (backend scaling)
B. Edge cache00Niski (5-20% hit ratio)Najlepszy cost/performance ratio dla typowych shopów
C. ISR z MemoryQueueR2 storage + Class A/B ops0NiskiŚredni — R2 cost rośnie z catalog size
C+. ISR z doQueueR2 + DO storage + DO opsDO requests $0.15/MNiskiWysoki dla high-traffic — DO ops dominate przy ramping
D. SSG0 (assets CDN)0Bardzo 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

ModelTime-to-fresh dla returning visitorTime-to-fresh dla new visitor
A. Pure SSRNatychmiast (next request)Natychmiast
B. Edge cache (s-maxage=300)Po 5 min lub po CF cache purge APIPierwszy request w PoP = fresh
C. ISR (revalidate=60)Po 60s + 1 więcej request (background regen) lub webhookPierwszy request after cache miss
D. SSG + webhookNatychmiast 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.

Świeżość po wdrożeniu nowej wersji storefronta

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

ModelSetupDebugFailure modeRecovery
ANoneBrowser DevTools, backend logsBackend GraphQL outage → 500 dla użytkownikaAuto po backend recovery
BMiddleware Cache-Control headersCF Edge cache visible w CF dashboardStale content po cache key bump bez purgeCF Cache Purge API or wait s-maxage window
COpenNext queue + R2 binding + cache interceptionOpenNext debugCache + Worker LogsQueue failure → log IgnorableError, stale content niezauważonePer-page revalidatePath() manual flush
C+ doQueueOpenNext queue + R2 + Durable Object class + migration tagDO state debugging trudnyDO migration drift → deploy fail, lub orphaned DO stateManual CF dashboard DO inspection + force migration tag bump
DgenerateStaticParams + webhook handlerBuild logs, webhook delivery logsWebhook delivery failure → stale content do next build/deployRe-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-maxage window 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: 60 na hot products, revalidate: 3600 na 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

LimitWartośćImpact na model
Workers requests / month10MModel A/B mocno limited przez backend load. ISR oszczędza Worker invocations.
CPU time per request30s (Bundled)Pure SSR ryzyko 1101 przy burst
Sub-requests per invocation10,000All models OK
R2 free tier10 GB + 1M Class A + 10M Class B / m-cISR storage cost rośnie z catalog size
Durable ObjectsPaid 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 (fetch z revalidate/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:

  1. Hosting współdzielony = Model A lub B (no Next.js ISR) — multi-tenant collision risk znika
  2. Custom queue wrapper z tenant-scoped MessageGroupId = ${hostname}_${route} — wymaga custom JS code w open-next.config.ts
  3. 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:

ZmiennaPure SSREdge cacheISRSSG+webhook
Catalog sizedowolnydowolnymały-średnimały-średni (build time)
Traffic< 50 RPS50-1000 RPS100-10000+ RPSdowolny
Update frequencydowolnygodzinowy+godzinowy/dziennygodzinowy+ (webhook trigger)
Real-time freshness wymógTAKNIE (5 min OK)NIE (60s OK)TAK (webhook)
Operacyjna złożoność tolerancjaNISKANISKAŚREDNIAWYSOKA
LCP target< 2.5s< 1.5s< 1s< 0.5s
Backend GraphQL capacitywysokaśrednianiskaminimal
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)

  1. Dodaj middleware.ts z Cache-Control: s-maxage=300 dla wybranych routes
  2. Deploy
  3. Cache się rozgrzewa automatically po requestach

Czas: ~10 min implementation, ~5 min deploy

B → C (Edge cache → ISR)

  1. Usuń middleware.ts cache headers (uniknij konfliktu)
  2. Dodaj export const revalidate = N per route
  3. Verify open-next.config.ts ma r2IncrementalCache + queue: memoryQueue + enableCacheInterception (CLI ≥1.1.2 default)
  4. doswiftly deploy — backend auto-provisionuje R2 bucket per shop
  5. 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)

  1. Dodaj generateStaticParams per route
  2. Set export const dynamicParams = false (lub true jeśli chcesz fallback)
  3. Add webhook handler dla revalidatePath() calls
  4. Backend webhook integration (DoSwiftly admin → Twój webhook endpoint po admin save)
  5. 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).

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 revalidate tuning 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.

Unikaj jednosegmentowej trasy /[handle] ze spekulatywnym fan-outem

Trasa 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