Back to Blog
Implementation Guide

Server-Side A/B Testing in Next.js 14 App Router (No Flicker)

Updated December 2025
15 min read
TL;DR

The short answer: To A/B test in Next.js 14 App Router without flicker, use Edge Middleware to assign variants server-side. Set a cookie with the variant, then read it in your Server Components. This approach adds <5ms latency and zero Cumulative Layout Shift (CLS).

Who this is for

  • Next.js developers using the App Router (Next.js 13.4+)
  • Teams who need flicker-free A/B testing
  • Performance-conscious developers (Core Web Vitals matter)
  • Anyone using React Server Components

Who this is NOT for

A/B Testing Approaches Compared

ApproachLatencyFlickerComplexityVerdict
Edge Middleware (Recommended)<5msNoneMedium Recommended
Client-Side SDK100-500msYes (FOOC)Low
Server Component Props<10msNoneHigh

Prerequisites

  • Next.js 13.4+ with App Router enabled
  • Deployed to Vercel, Cloudflare, or any Edge-compatible platform
  • Basic understanding of TypeScript and React Server Components

Step-by-Step Implementation

1

Create the Middleware

Create middleware.ts in your project root. This runs on the Edge before any page renders.

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// Define your experiments
const EXPERIMENTS = {
  'hero-headline': {
    variants: ['control', 'variant-a', 'variant-b'],
    weights: [0.34, 0.33, 0.33], // Traffic split
  },
  'pricing-layout': {
    variants: ['control', 'horizontal'],
    weights: [0.5, 0.5],
  },
}

function getVariant(experimentId: string): string {
  const experiment = EXPERIMENTS[experimentId as keyof typeof EXPERIMENTS]
  if (!experiment) return 'control'
  
  const random = Math.random()
  let cumulative = 0
  
  for (let i = 0; i < experiment.variants.length; i++) {
    cumulative += experiment.weights[i]
    if (random < cumulative) {
      return experiment.variants[i]
    }
  }
  
  return experiment.variants[0]
}

export function middleware(request: NextRequest) {
  const response = NextResponse.next()
  
  // Check for existing experiment cookies
  for (const [experimentId] of Object.entries(EXPERIMENTS)) {
    const cookieName = `exp_${experimentId}`
    const existingVariant = request.cookies.get(cookieName)?.value
    
    if (!existingVariant) {
      // Assign new variant
      const variant = getVariant(experimentId)
      response.cookies.set(cookieName, variant, {
        httpOnly: false, // Allow client-side reading for analytics
        secure: true, // Use secure cookies in production
        sameSite: 'lax',
        maxAge: 60 * 60 * 24 * 30, // 30 days
      })
    }
  }
  
  return response
}

export const config = {
  matcher: [
    // Match all paths except static files and API routes
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

⚠️ Important: Edge Middleware runs on every request. Keep it fast—avoid database calls or heavy computation.

2

Create a Server-Side Helper

Create a utility to read experiment variants in Server Components.

// lib/experiments.ts
import { cookies } from 'next/headers'

export function getExperimentVariant(experimentId: string): string {
  const cookieStore = cookies()
  const variant = cookieStore.get(`exp_${experimentId}`)?.value
  return variant || 'control'
}

// Type-safe variant getter
export function getHeroVariant(): 'control' | 'variant-a' | 'variant-b' {
  return getExperimentVariant('hero-headline') as 'control' | 'variant-a' | 'variant-b'
}

export function getPricingVariant(): 'control' | 'horizontal' {
  return getExperimentVariant('pricing-layout') as 'control' | 'horizontal'
}
3

Use in Server Components

Read the variant in your Server Component and render accordingly.

// app/page.tsx
import { getHeroVariant } from '@/lib/experiments'

const headlines = {
  'control': 'Build Better Products with A/B Testing',
  'variant-a': 'Increase Conversions by 30% with Data-Driven Decisions',
  'variant-b': 'Stop Guessing. Start Testing.',
}

export default function HomePage() {
  const variant = getHeroVariant()
  const headline = headlines[variant]
  
  return (
    <main>
      <h1 className="text-4xl font-bold">
        {headline}
      </h1>
    </main>
  )
}
4

Track Conversions

Send experiment data to your analytics platform when conversions happen.

// components/CTAButton.tsx
'use client'

function getCookie(name: string): string | null {
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
  return match ? match[2] : null
}

export function CTAButton() {
  const handleClick = () => {
    const variant = getCookie('exp_hero-headline') || 'control'
    
    // Send to your analytics
    fetch('/api/track', {
      method: 'POST',
      body: JSON.stringify({
        event: 'cta_click',
        experiment: 'hero-headline',
        variant,
      }),
    })
    
    // Or use ExperimentHQ
    window.experimenthq?.track('cta_click', {
      experiment: 'hero-headline',
      variant,
    })
  }
  
  return (
    <button onClick={handleClick}>
      Get Started Free
    </button>
  )
}

Common Mistakes to Avoid

❌ Using Math.random() in Server Components

Server Components can render multiple times. Put randomization in middleware only, then read from cookies.

❌ Not persisting variants

Users should see the same variant across sessions. Use cookies with a 30-day expiry minimum.

❌ Caching variant-specific pages

Don't use static generation for pages with experiments. Use export const dynamic = 'force-dynamic' or ensure proper cache headers.

❌ Ignoring bot traffic

Filter out bots from your experiment data. Check user-agent in middleware and skip variant assignment for crawlers.

Performance Impact

<5ms

Added latency

0

CLS impact

~1KB

Bundle size

Edge Middleware runs in <50ms globally. Compare this to client-side A/B testing tools that add 100-500ms latency and cause visible layout shifts.

Our Recommendation

For Next.js App Router projects, Edge Middleware is the best approach for A/B testing. It's:

  • Flicker-free — variants assigned before render
  • Fast — <5ms latency at the edge
  • SEO-safe — no CLS, no duplicate content issues
  • RSC-compatible — works with Server Components

If you want a managed solution with visual editing, analytics, and statistical significance calculations, try ExperimentHQ — it integrates with Next.js in under 5 minutes.

Frequently Asked Questions

How do I prevent flicker in Next.js A/B tests?
Use Edge Middleware to assign variants server-side before the page renders. This ensures users see the correct variant immediately without any flash of original content (FOOC).
Can I A/B test React Server Components?
Yes. Pass the variant as a cookie or header from middleware, then read it in your Server Component using cookies(). The variant is determined before React renders, so there's no hydration mismatch.
Does A/B testing affect Next.js Core Web Vitals?
Server-side A/B testing via Edge Middleware has minimal impact on Core Web Vitals. Client-side testing tools often add 100-500ms latency and cause CLS issues. Edge-based testing adds <5ms.
How do I handle caching with A/B tests?
Add export const dynamic = 'force-dynamic' to pages with experiments, or use the Vary: Cookie header to cache different variants separately.

Share this article

Ready to start A/B testing?

Free forever plan available. No credit card required.