09 — Caching & Performance Strategy
Status: 📐 Target design — the current mock/localStorage build has no server cache layer to configure (there's no real data-fetching to cache yet); this document specifies the caching and performance approach for when Supabase/Prisma land.
🟢 বাংলা ভূমিকা: এই ডকুমেন্টে Upstash Redis ক্যাশিং, Next.js ISR/SSG কৌশল প্রতিটি রুটের জন্য, এবং Core Web Vitals টার্গেট বর্ণনা করা হয়েছে।
0. What is true today
Sixteen routes prerender; the rest are server-rendered on demand. That split
was not free, and the way it was lost is worth recording: reading the
NEXT_LOCALE cookie inside not-found.tsx to translate the 404 opted every
route in the app out of static rendering — the marketing site, the docs, the
whole platform-admin section — because cookies() in a segment's not-found
makes everything beneath that segment dynamic. All 42 routes were ƒ.
The fix kept the translated 404 and removed the cost: the locale-scoped
not-found pages are client components reading the copy from ErrorCopyProvider,
which the layout above them already supplies with the right language, and the
root one (which has no layout, so no provider) ships both languages' error copy
and picks between them from the cookie on the client.
Tenant routes stay dynamic and should: /table reads searchParams, and the
rest resolve a tenant from the URL. Those are the routes ISR is aimed at once
menus come from a database rather than a module.
🟢 বাংলা সারাংশ: এখন ১৬টি রুট prerender হয়, বাকিগুলো প্রতি অনুরোধে সার্ভার-রেন্ডার। এটা একবার পুরো হারিয়ে গিয়েছিল — 404 অনুবাদ করার জন্য not-found.tsx-এ NEXT_LOCALE কুকি পড়া হয়েছিল, আর কোনো সেগমেন্টের not-found-এ cookies() থাকলে তার নিচের সব রুট dynamic হয়ে যায়; ফলে ৪২টি রুটের সবগুলোই ƒ হয়ে গিয়েছিল। অনুবাদ ঠিক রেখেই খরচটা সরানো হয়েছে।
1. Rendering Strategy Per Route
Cross-referencing the full route table in 02-folder-and-routing.md §3, each route gets a deliberate rendering/caching strategy based on how often its data changes and how tenant/personalization-sensitive it is:
| Route | Strategy | Revalidation | Rationale |
|---|---|---|---|
/[locale] (Landing) | SSG | On deploy | Fully static marketing content, no per-tenant data |
/[locale]/platform-admin/* | SSR, no cache | n/a | Always-fresh admin data, low traffic, correctness > speed |
/tenants/[tenant]/dashboard/* | SSR, no cache | n/a | Owner-only, always-fresh operational data |
/tenants/[tenant]/table (menu) | ISR | revalidate: 60s + on-demand tag invalidation on menu edit | High read traffic, low write frequency (menu changes rarely) — ISR gives near-static speed with acceptable staleness |
/tenants/[tenant]/promo/[promoId] | ISR | revalidate: 300s + on-demand invalidation on promo edit | Marketing pages shared widely (social, flyers) — must be fast for cold, anonymous traffic; content rarely changes after publish |
/tenants/[tenant]/kitchen, /waiter, /rider | SSR (client-hydrated, Realtime-driven) | n/a | Must reflect live order state; initial HTML is a shell, Realtime subscription takes over |
/[locale]/docs/[slug] | SSG | On deploy | Static documentation content read from the filesystem at build/request time |
On-demand revalidation uses Next.js revalidateTag/revalidatePath, triggered from the relevant dashboard mutation (e.g. saving a menu item calls revalidateTag('menu:{tenantId}') immediately, so an owner never has to wait out the 60s ISR window to see their own edit reflected on /table).
🟢 বাংলা সারাংশ: ল্যান্ডিং পেজ সম্পূর্ণ স্ট্যাটিক (SSG), ড্যাশবোর্ড/প্ল্যাটফর্ম-অ্যাডমিন সবসময় ফ্রেশ ডেটা লোড করবে (SSR, no cache), মেনু ও প্রোমো পেজ ISR (৬০-৩০০ সেকেন্ড পর পর রিভ্যালিডেট + এডিট করলে সাথে সাথে on-demand ইনভ্যালিডেশন), এবং কিচেন/ওয়েটার/রাইডার অ্যাপ রিয়েল-টাইম সাবস্ক্রিপশনের উপর নির্ভর করবে।
2. Upstash Redis Caching Layer
Used for data that's expensive to (re)compute or fetch but doesn't need Postgres-grade consistency guarantees:
| Cache key pattern | TTL | Purpose |
|---|---|---|
menu:{tenantId} | 5 min | Serialized full menu (categories + items + variants/addons) — avoids a join-heavy query on every /table visit |
tenant:slug:{slug} | 1 hour | Tenant lookup by subdomain — hit on every single request via proxy.ts, so this is the single hottest cache key in the system |
tenant:domain:{customDomain} | 1 hour | Same, for custom-domain resolution |
analytics:{tenantId}:{range} | 15 min | Precomputed analytics rollups (revenue trend, BCG matrix inputs) — expensive aggregate queries, tolerant of staleness |
feature-flags:{tenantId} | 1 min | Resolved effective feature-flag map (global × plan-whitelist × tenant override, see 03 §4) — short TTL since admin changes should propagate fast |
rate-limit:{ip or tenantId}:{route} | sliding window | Rate limiting counters (see 12-security-audit-and-rate-limiting.md) |
// lib/cache/tenant-lookup.ts — the hottest path, called from proxy.ts on every request
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
export async function resolveTenantBySlug(slug: string) {
const cached = await redis.get<Tenant>(`tenant:slug:${slug}`);
if (cached) return cached;
const tenant = await prisma.tenant.findUnique({ where: { slug } });
if (tenant) await redis.set(`tenant:slug:${slug}`, tenant, { ex: 3600 });
return tenant;
}
export async function invalidateTenantCache(slug: string) {
await redis.del(`tenant:slug:${slug}`); // called whenever tenant settings are saved from the dashboard
}
Because proxy.ts runs on every single request (it's the subdomain resolver), this lookup is the single highest-QPS read in the whole system — an Upstash-backed cache here (rather than hitting Postgres per request) is the single highest-leverage caching decision in the platform.
🟢 বাংলা সারাংশ: Upstash Redis-এ সবচেয়ে গুরুত্বপূর্ণ ক্যাশ হলো tenant:slug:{slug} — কারণ proxy.ts প্রতিটি রিকোয়েস্টে এটা লুকআপ করে সাবডোমেইন রিজলভ করার জন্য। এছাড়া মেনু, অ্যানালিটিক্স, এবং ফিচার ফ্ল্যাগের জন্যও ক্যাশ থাকবে, প্রতিটির নিজস্ব TTL সহ।
3. Core Web Vitals Targets
| Metric | Target | Route class most at risk |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.0s | /table (menu hero image), /promo/[promoId] (banner) |
| INP (Interaction to Next Paint) | < 200ms | /table cart interactions, KDS status-advance taps |
| CLS (Cumulative Layout Shift) | < 0.1 | Menu grid (image aspect-ratio boxes reserved), dashboard tables |
| TTFB (Time to First Byte) | < 400ms (cached), < 800ms (cache miss) | Any ISR/SSR route |
Concrete techniques already applicable to the current mock build (no backend dependency):
next/imagewith explicitwidth/height(orfill+ aspect-ratio container) everywhere to eliminate CLS from menu/promo images.- Route-level code-splitting is automatic per Next.js App Router segment; heavy dashboard-only chart libraries (e.g. analytics charting) should stay dynamically imported (
next/dynamic) so/table's bundle never pays for dashboard-only code. - Framer Motion animations should prefer
transform/opacity(GPU-composited) over layout-triggering properties to keep INP low during cart-sheet/modal transitions.
🟢 বাংলা সারাংশ: LCP ২ সেকেন্ডের নিচে, INP ২০০ মিলিসেকেন্ডের নিচে, এবং CLS ০.১-এর নিচে রাখার লক্ষ্য। এখনই প্রয়োগযোগ্য কৌশল — সব ছবিতে next/image ও নির্দিষ্ট width/height ব্যবহার করা, ড্যাশবোর্ড-ভারী কম্পোনেন্ট (যেমন চার্ট) dynamic import করা যাতে কাস্টমার-সাইড বান্ডেল হালকা থাকে, এবং Framer Motion অ্যানিমেশনে transform/opacity ব্যবহার করা।
4. Database Query Performance (once Prisma lands)
- N+1 avoidance: menu queries always
includecategories/variants/addons in a single Prisma call rather than per-item round trips — the mock data's already-nested shape (MenuItem.variants,.addons) is intentionally designed to map to a single eager-loaded query. - Read replicas for analytics/reporting queries (heavy aggregates) so they never contend with the hot order-write path during peak dinner-rush traffic.
- Connection pooling via Supabase's PgBouncer (transaction mode) — required because serverless Next.js API routes/Vercel Functions open/close connections far more frequently than a traditional long-lived server process.
🟢 বাংলা সারাংশ: মেনু কোয়েরিতে N+1 সমস্যা এড়াতে একবারে সব রিলেটেড ডেটা (ক্যাটাগরি, ভ্যারিয়েন্ট, অ্যাডঅন) লোড করা হবে। ভারী অ্যানালিটিক্স কোয়েরির জন্য read replica ব্যবহার করা হবে যাতে ব্যস্ত সময়ে অর্ডার লেখার সাথে কনফ্লিক্ট না হয়, এবং Supabase-এর PgBouncer দিয়ে কানেকশন পুলিং করা হবে (Vercel Functions-এর জন্য জরুরি)।