TL;DR
Next.js App Router A/B testing: Use middleware to assign variants at the edge, set cookies for consistency, and rewrite to variant-specific pages or pass variant via headers. This gives you zero-flicker server-side testing. Works with edge runtime and Vercel Edge Functions.
The Middleware Approach
Next.js middleware runs at the edge before the page renders. Perfect for A/B testing:
- User visits page → middleware intercepts request
- Assign variant → hash user ID or set cookie
- Rewrite or set header → pass variant to page
- Render correct variant → zero flicker
Implementation Example
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Get or create user ID
let userId = request.cookies.get('user_id')?.value
if (!userId) {
userId = crypto.randomUUID()
}
// Assign variant (50/50 split)
const hash = simpleHash(userId + 'experiment-1')
const variant = hash % 2 === 0 ? 'control' : 'variant'
// Set cookie for consistency
const response = NextResponse.next()
response.cookies.set('user_id', userId, { maxAge: 31536000 })
response.cookies.set('experiment-1', variant, { maxAge: 31536000 })
// Pass variant to page via header
response.headers.set('x-experiment-variant', variant)
return response
}
// Simple hash function
function simpleHash(str: string): number {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i)
hash = hash & hash
}
return Math.abs(hash)
}
export const config = {
matcher: '/pricing',
}Using Variant in Page Component
// app/pricing/page.tsx
import { cookies } from 'next/headers'
export default function PricingPage() {
const variant = cookies().get('experiment-1')?.value || 'control'
return (
<div>
{variant === 'control' ? (
<h1>Choose Your Plan</h1>
) : (
<h1>Start Your Free Trial</h1>
)}
{/* Rest of page */}
</div>
)
}Caching Considerations
App Router caching can interfere with A/B tests:
- • Use dynamic rendering: Add
export const dynamic = 'force-dynamic' - • Or use cookies: Reading cookies makes the page dynamic automatically
- • CDN caching: Ensure Vary: Cookie header is set
- • Test in production: Dev mode behaves differently
Pros
- • Zero flicker (server-side)
- • Runs at the edge (fast)
- • Full control over logic
- • Works with RSC
Cons
- • Requires code changes
- • No visual editor
- • Caching complexity
- • Edge runtime limitations
Easier Alternative
For most Next.js sites, client-side testing with ExperimentHQ is simpler:
- • No code changes needed
- • Visual editor for quick tests
- • Minimal flicker with <5KB script
- • Works with App Router out of the box