Personalisation is the feature every marketing leader wants and almost no team ships well. The naive approach (server-render per user) tanks performance. The fully-static approach hits a ceiling at "different homepage hero per region". The right architecture lives at the edge.
This piece is the reference we apply on Next.js + Vercel personalisation engagements. It covers what to personalise at the edge vs the origin vs the client, the middleware patterns that keep Lighthouse scores intact, the cache-key discipline that prevents cache pollution, and the A/B testing layer that produces statistically defensible learnings.
Why most personalisation is over-engineered
Three failure modes recur:
"We're personalising everything". every page section, every CTA, every product recommendation is dynamic. Result: cache-hit rate drops to 5 percent, p95 latency triples, the engineering team spends three sprints fighting performance.
"We're A/B testing every feature". six concurrent experiments overlap, the analytics team cannot disentangle which lift came from which test, the marketing team loses faith in the data and reverts to gut decisions.
"We bought a personalisation platform". Optimizely, Dynamic Yield, Mutiny, or a homegrown vendor sits in front of the storefront, adds 200-400ms to first paint, breaks the brand's Core Web Vitals, paid media performance silently degrades.
The pattern under all three is the same: personalisation gets adopted as a marketing initiative, not an engineering architecture. The fix is architectural discipline, not better vendors.
The three layers: edge, origin, client
Personalisation lives at one of three layers. Each has its own use case and its own cost profile.
Edge personalisation (Vercel middleware, Cloudflare Workers, Lambda@Edge)
The request reaches a CDN POP. Middleware reads request headers (geo from IP, user-agent for device, cookies for cohort), makes a routing decision in single-digit milliseconds, rewrites the URL or sets request headers, and forwards to the origin.
Right for
- Geo routing (India sees Hindi options, GCC sees Arabic, UK sees GBP pricing)
- Device routing (mobile vs desktop content shape)
- Cookie-based cohort routing (returning vs new visitor)
- A/B test assignment
- Bot detection + traffic shaping
Wrong for
- Per-user dynamic content (the cache breaks)
- Heavy data lookups (the middleware has tight CPU + memory limits)
- Anything requiring fresh CMS reads on every request
Vercel middleware runs in the Edge Runtime (V8 isolate), restricted API surface, ~10-50ms typical execution time.
Origin personalisation (Next.js Server Components, ISR with cache tags)
The request reaches your Next.js app on Vercel. Server Components fetch personalised data, render the page, return the HTML. Cache tags + on-demand revalidation handle freshness.
Right for
- Logged-in user dashboards
- Cart + checkout
- Account pages
- Anything requiring database reads + business logic
Wrong for
- Public marketing pages where ISR + static would serve as well
- Per-user pages without authentication (use edge instead)
Origin runs in Node runtime, full API surface, 100-500ms typical render time.
Client personalisation (React state, browser fetch)
The page renders + paints. JavaScript on the client fetches personalisation data + swaps content in place.
Right for
- Post-paint personalisation that doesn't block first contentful paint
- Recommended product carousels on category pages
- Recently viewed products
- Logged-in user preferences applied after auth check
Wrong for
- Above-the-fold content (the layout shift kills Core Web Vitals)
- Anything search engines need to see (server-side is the right default for SEO)
When does edge personalisation pay off?
Edge wins on three criteria:
- The personalisation decision is fast (single-digit-ms compute)
- The number of variants is small (you can cache each variant)
- The variant choice is deterministic from request data (no database lookup needed)
Geo routing satisfies all three. Cookie-based cohort assignment satisfies all three. Device routing satisfies all three. A user-level recommendation engine satisfies NONE of the three; it belongs at the origin or client layer.
The middleware pattern that doesn't kill performance
Vercel middleware runs on every request to the matched paths. Mistakes here multiply across millions of requests.
Pattern 1: rewrite, don't redirect
// middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
const country = request.geo?.country || "IN"
if (request.nextUrl.pathname === "/") {
if (country === "AE" || country === "SA") {
return NextResponse.rewrite(new URL("/regional/gcc", request.url))
}
if (country === "GB") {
return NextResponse.rewrite(new URL("/regional/uk", request.url))
}
return NextResponse.rewrite(new URL("/regional/in", request.url))
}
}
export const config = {
matcher: ["/"],
}
The URL stays / in the browser. The cache key includes the rewrite target so each variant caches independently. SEO sees one canonical URL. The user sees regional content.
Pattern 2: cookie-stamped cohort assignment
export function middleware(request: NextRequest) {
let cohort = request.cookies.get("ab-cohort")?.value
if (!cohort) {
cohort = Math.random() < 0.5 ? "control" : "variant"
const response = NextResponse.next()
response.cookies.set("ab-cohort", cohort, {
maxAge: 60 * 60 * 24 * 30,
httpOnly: false,
})
return response
}
const response = NextResponse.next()
response.headers.set("x-ab-cohort", cohort)
return response
}
First-time visitor gets a coin flip + cookie. Returning visitors get their assigned cohort. The Next.js page reads x-ab-cohort and renders accordingly. Cache key includes the header so each variant caches.
Pattern 3: bot filtering + analytics tagging
const BOT_USER_AGENTS = /bot|crawler|spider|scraper|googlebot|bingbot/i
export function middleware(request: NextRequest) {
const ua = request.headers.get("user-agent") || ""
if (BOT_USER_AGENTS.test(ua)) {
const response = NextResponse.next()
response.headers.set("x-is-bot", "true")
return response
}
}
Downstream analytics tags bot traffic separately so it doesn't pollute conversion data.
What to avoid in middleware
- Database queries (no native database access in Edge Runtime; even if you bring an HTTP client it adds 50-200ms latency)
- Heavy computation (CPU caps)
- Large payloads (memory caps)
- Awaiting multiple external calls in sequence (cumulative latency)
The rule: middleware should make a routing decision in 20ms or less. Anything heavier belongs at the origin.
Cache-key discipline
The single biggest gotcha with edge personalisation: cache pollution.
If your homepage caches under one key, and middleware rewrites it to 3 regional variants, the cache must understand the 3 variants are different. Otherwise variant A gets served to variant B users (or worse, variant A overwrites the cached variant B).
How Vercel handles it (mostly)
Vercel's CDN cache automatically respects:
- Different URLs (after middleware rewrite)
- Different
Varyresponse headers - Different cookies in the cookie allowlist
The pattern that works:
- Middleware rewrites
/to/regional/in(or/regional/gcc, etc.) - Next.js renders
/regional/inas a fully static page - Vercel caches
/regional/inonce globally - Subsequent requests to
/from India hit the same cache - Subsequent requests to
/from GCC hit a different cache (different rewrite target)
The cache-hit rate stays at ~95 percent. Edge latency stays in the sub-50ms range. Lighthouse stays green.
Patterns that break the cache
- Per-user middleware rewrites (cache fragments to 1-per-user, no shared cache)
- Cookie-based content variation without proper Vary headers
- Setting cookies in middleware that vary per request
- Server Components fetching personalised data on every render without ISR or cache tags
A/B testing at the edge
Edge A/B tests work when:
- The variant is determined by middleware (cookie assignment, geo, device)
- The variant is rendered as a separate cached URL or differentiated by request header
- Analytics tags the variant on every event for the user's session
Statistical discipline
The most common A/B testing failure mode is statistical sloppiness:
- Peeking at the data daily and stopping when you see "significance". this destroys the significance test's validity. Pre-register the sample size + duration; do not peek.
- Running concurrent overlapping tests. Test A affects Test B's measurement. Either serialise tests or use proper multi-variate analysis.
- Optimising for the wrong metric. testing for click-through when conversion is what matters. Click-through can move in the opposite direction of conversion.
Tools that handle the statistics correctly: GrowthBook (open-source, self-hostable, free at small scale), Statsig, LaunchDarkly, Vercel's own A/B testing primitives + a custom analytics layer.
Holdout patterns
For high-stakes tests (homepage redesign, checkout flow change, pricing experiments), run a long-running holdout group: 5-10 percent of users perpetually see the OLD version. Compare lifetime metrics + retention curves between holdout + treated cohorts. This is how you catch "winners that decay" or "losers that lift over time".
What the architecture looks like in production
For a D2C brand at Rs 50 crore ARR running personalisation at scale:
| Layer | What lives here | Latency budget |
|---|---|---|
| Vercel middleware | Geo routing, device routing, cookie-based cohort assignment, A/B test bucketing | 20ms |
| Next.js static + ISR | Regional homepage variants, category pages, blog, marketing pages | Cache hit; ~10ms TTFB |
| Next.js Server Components + ISR with tags | Logged-in dashboard, cart preview, account pages | 100-300ms TTFB |
| Client React + fetch | Product carousels, recently viewed, recommendation panels | Post-paint, async |
| GrowthBook (or similar) | A/B test config + statistical analysis | Async, doesn't block render |
| Vercel Analytics + GA4 + warehouse | Per-variant metrics, conversion attribution, cohort retention | Daily warehouse refresh |
Lighthouse scores stay at 95+. Largest Contentful Paint under 1.2 seconds on median 4G. Cumulative Layout Shift under 0.1. The brand gets meaningful personalisation without sacrificing the performance baseline.
Production checklist
For a Next.js + Vercel programme rolling out personalisation:
- Personalisation goals documented per page type (which variants, which audience, what success metric)
- Layer assignment per personalisation (edge / origin / client) against the criteria above
- Vercel middleware deployed with geo + device + cohort routing logic
- Cache-key discipline validated: cache-hit rate per variant > 90 percent in production
- A/B testing tool selected (GrowthBook / Statsig / native) + integrated
- Pre-registration discipline: sample size + duration + primary metric agreed before test launches
- Bot filtering + analytics tagging discipline (variant tagged on every event in GA4 + warehouse)
- Monthly review: tests running, tests concluded, winners shipped, losers retired
- Quarterly holdout review: long-running holdout cohort metrics vs treated cohort
- Performance budget: Lighthouse 90+, LCP < 1.5s, CLS < 0.1; enforced as a CI gate
- Documentation: every active personalisation rule + its rationale + its retirement criterion
References + linked context
- Dcrayons glossary: edge-runtime, isr, jamstack, content-modelling
- Dcrayons CMS architectures: Enterprise Vercel reference architecture, Enterprise Contentful + Next.js
- Mid-market guide: How to Pick a Headless CMS in 2026
If your marketing team is asking for personalisation and your engineering team is worried about Lighthouse scores, the answer is architectural, not vendor selection. Reach out via the contact form for a 30-minute review of your current setup.



