Guide

How to run A/B tests in Next.js without flicker

To A/B test in Next.js without flicker, decide the variant before the page paints. Either decide on the server (a flag read in a dynamic server component, or a proxy rewrite to a pre-rendered variant, as Vercel's Flags SDK precompute pattern does), or use a synchronous script in the root layout's head that hides only the tested elements and waits for hydration. Duelpilot offers both: a hydration-safe script for visual tests and a server-side flags SDK for code tests.

Last reviewed Markdown version

Why Next.js tests flicker or break hydration

There are two ways a test goes wrong in Next.js. The first is flicker: a client-side testing tool that loads after the first paint (an async tag, next/script, a useEffect) lets visitors see the original for a moment before the variant replaces it. That biases the test as well as looking broken (more on flicker).

The second is hydration. Next.js sends server-rendered HTML and React then hydrates it. A hydration error occurs when the React tree rendered on the server differs from the first render in the browser, and scripts or extensions that modify the HTML are one of the documented causes. So a tool that changes server-rendered text before React has hydrated it can make React throw the HTML away and re-render, which brings the original back. A flicker-free test has to be early and hydration-aware at the same time.

The options, vendor-neutral

ApproachHow it avoids flickerTrade-off
Flag in a server componentThe server renders the visitor's arm, so server HTML and the hydrated tree agree.Reading cookies() in a page or layout opts the route into dynamic rendering.
Proxy (middleware) rewriteProxy runs before routes render and can rewrite a request to another route, for example /home-b, while the URL stays the same.One route per variant to maintain. In Next.js 16 middleware is renamed proxy and defaults to the Node.js runtime; static export doesn't support it.
Flags SDK precomputeProxy evaluates the flags, encodes the result in a short code and rewrites to a [code] route, so pages stay static (flags-sdk.dev).Needs a FLAGS_SECRET and a flag provider or your own decide functions; every flag combination is a page, built ahead or cached with ISR.
Synchronous script in the headRuns while the browser parses the head, before body HTML paints, and changes the page in the browser.Must not change text React is about to hydrate; best for visual changes (copy, layout, images, styles).
Ways to run A/B tests in a Next.js app. Each Next.js or Vercel behaviour links to its documentation.

Server-side decisions are the right home for behaviour: a new checkout step, fewer form fields, pricing logic, search ranking. A browser script is the faster route for visual changes on marketing pages, because nobody has to write a route per variant. The Flags SDK works with providers such as LaunchDarkly and Statsig as well as custom adapters.

Duelpilot for visual tests: a hydration-safe script

Add a plain synchronous script to the head of the root layout (app/layout.tsx, or pages/_document.tsx in the Pages Router), not next/script. Next.js documents that a beforeInteractive script does not block hydration (Script component), and in our tests visitors still saw the server-rendered original before the variant; afterInteractive, the default, loads after some hydration. Keep it in the server-rendered layout, never in a "use client" component.

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        {/* eslint-disable-next-line @next/next/no-sync-scripts -- Duelpilot must run before first paint (no flicker) */}
        <script data-cfasync="false" src={process.env.NEXT_PUBLIC_DUELPILOT_SRC} />
      </head>
      <body>{children}</body>
    </html>
  );
}
  • Anti-flicker per element: only the elements under test, in every version including the original, are hidden by a CSS rule, and each is revealed as soon as its change is applied. The page itself is never hidden. A failsafe reveals everything after 1,200 ms by default (a site setting between 300 and 3,000 ms).
  • Hydration-safe mode: Duelpilot recognises Next.js from its /_next/ assets. CSS, class, attribute and image changes apply at once; text and structural changes wait, still hidden, until React has hydrated that element, so no hydration errors. If hydration hasn't happened by the failsafe, the remaining changes are applied then.
  • Fair timing: when one version waits for hydration on an element, every version waits on it, so A and B appear at the same moment and the slower one isn't handicapped.
  • Client-side navigation: changes re-apply when React re-renders an element and when a <Link> navigation changes the route.
  • Strict CSP: give the tag your nonce (read it from headers() in the layout) and Duelpilot reuses it for its <style> elements.

Showing one line at first paint

On a slow phone, hydration can take a second or two, capped by the failsafe. For one important line, such as the hero headline, render its content as a constant with dangerouslySetInnerHTML and add data-dp-early. React never hydrates that content, so Duelpilot changes it before hydration and every version shows at first paint. Never put user input there, and never use it on an element whose children React renders as JSX.

<h1 data-hero="headline" data-dp-early="" dangerouslySetInnerHTML={{ __html: HEADLINE_HTML }} />

Your AI agent installs this as a pull request and runs verify_install on a static and a dynamic route. It fails a tag that went through next/script (async, defer or data-nscript) or sits in the body, and checks the CSP.

Duelpilot for server-side tests: the Code plan

For behaviour changes, your own agent builds each arm in your repository behind a Duelpilot flag and opens the pull requests; Duelpilot never gets access to your code and stores only links. The server SDK is one dependency-free TypeScript file that runs on Node 18+, Bun, Deno, Cloudflare Workers and Vercel (Node and Edge runtimes). It buckets exactly like the browser script, using a shared dp_vid cookie that your app sets where consent allows, and null always means control. The recommended pattern: decide on the server without counting, and count the exposure in the browser.

// app/checkout/page.tsx (server component)
import { cookies, headers } from "next/headers";
import { createFlags } from "@/lib/duelpilot-flags";

const flags = createFlags({ siteKey: process.env.NEXT_PUBLIC_DUELPILOT_SITE_KEY!, edgeUrl: process.env.NEXT_PUBLIC_DUELPILOT_EDGE!, count: false });

export default async function Checkout() {
  const arm = await flags.variant("e12", {
    visitorId: (await cookies()).get("dp_vid")?.value ?? "",
    userAgent: (await headers()).get("user-agent"),
    consent: true, // or read your consent cookie when the consent mode is not "off"
  });
  return (
    <>
      <Expose flag="e12" /> {/* client component: calls window.duelpilot.variant("e12") */}
      {arm === "v1" ? <InlineDatePicker /> : <DatePicker />}
    </>
  );
}

Each arm gets a preview deployment; Duelpilot runs site checks on it on phone, tablet and desktop, as long as the host is one of your domains or a pattern the owner allowed (such as myshop-*.vercel.app). A person approves every code test; they never run on autopilot. When a winner ships, the flag serves it to everyone and your agent opens a cleanup pull request that removes the flag. Code tests are on the Code plan (€199 per month); visual tests are on every plan, and it's €0 until you launch your first test.

Try Duelpilot with your own agent

Give your agent one line: set up Duelpilot for my site using /agents.md. It opens a workspace, installs and verifies the script, and drafts the first test; you approve it, then switch on autopilot if you like. Or see pricing.

€0 until you launch your first test

Questions

Why does my A/B test flicker in Next.js?

The testing script runs after the first paint: it is async, loaded through next/script, or started in a useEffect. Use a plain synchronous script in the root layout's head, or decide the variant on the server.

Can an A/B testing script cause hydration errors?

Yes, if it changes server-rendered text before React hydrates it. Duelpilot's hydration-safe mode applies style, class, attribute and image changes at once and holds text and structural changes, hidden, until React has hydrated that element.

Should I use middleware for A/B tests in Next.js?

It works: proxy (the Next.js 16 name for middleware) can rewrite each visitor to a variant route before rendering, and the Flags SDK's precompute pattern keeps those pages static. It suits code-level variants; for copy and layout tests a hydration-safe script saves building a route per variant.

Does Duelpilot work with the App Router and server components?

Yes. The script goes in the root layout's head, and hydration-safe mode covers server components too. For server-side tests, the flags SDK runs inside a server component, a route handler or a server action.

Sources

Last reviewed 2026-10-29. Spot an error? Tell us.

  1. Text content does not match server-rendered HTML | Next.js (checked 2026-10-29)
  2. proxy.js | Next.js (checked 2026-10-29)
  3. cookies | Next.js (checked 2026-10-29)
  4. Script Component | Next.js (checked 2026-10-29)
  5. Precompute | Flags SDK (checked 2026-10-29)
  6. Flags SDK Reference | Vercel Docs (checked 2026-10-29)