Next.js A/B testing

Next.js A/B testing: server-side flags or a client-side script

Next.js A/B testing comes in two shapes: server-side experiments that decide the variant before the page renders, usually with Vercel's Flags SDK, and client-side tools that change the page in the browser. This guide explains both, then shows how Otter installs in the App Router, follows client-side navigation, and when a server-side setup is the better choice.

Install location

app/layout.tsx (App Router) or pages/_document.tsx (Pages Router)

One shared installation powers every experiment, conversion goal, and report on this site.

  1. 01Copy the Next.js snippet from your Otter project's install page. It adds the anti-flicker style, a reveal failsafe, and the SDK through next/script with strategy="afterInteractive" and your key in data-optimo-key.
  2. 02Paste it into the <head> of app/layout.tsx, or into pages/_document.tsx if you use the Pages Router, then deploy.
  3. 03Verify the installation from Otter and create URL rules for the routes under test.
  4. 04Send product actions as goals with window.optimo.track('event_name') from your event handlers, and purchases with window.optimo.revenue().

Ideas

What to test on Next.js

Start with the changes closest to the money. These are the ones teams on this platform run first.

Marketing and pricing pages served by the App Router
Signup and onboarding steps that change routes without a reload
Split URL tests between two routes, such as /pricing and /pricing-b
Headline, call-to-action and layout changes without a redeploy
Targeted variants by UTM campaign, device or a custom visitor property
Goals fired from React event handlers with optimo.track()

The guide

Next.js A/B testing, explained

Next.js A/B testing: server-side or client-side?

Next.js has no built-in A/B testing product, so experiments run one of two ways. Server-side, your code decides the variant before rendering and sends only that version. Vercel's Flags SDK is the common route: a framework-native library for Next.js and SvelteKit that works with Vercel Flags or with Marketplace providers such as Statsig, PostHog, GrowthBook and LaunchDarkly. Client-side, a script loads in the browser, assigns the visitor and changes the page, which is how Otter and most visual testing tools work.

The Flags SDK's authors argue that flags belong on the server, because client-side flags cause layout shift: the page shows one version or a spinner and then swaps. Server-side testing avoids that, but every test costs engineering time and puts variants in your codebase. Client-side testing lets marketers change copy and layout without a deploy, at the cost of a short wait while the script decides. Many teams use both: flags for product features and a visual tool for marketing pages. We make Otter, the client-side tool described below, so weigh our view of the trade-offs accordingly.

How server-side tests work with Vercel's Flags SDK

The Flags SDK's precompute pattern keeps pages static. You evaluate flags in Proxy, which replaced the middleware file convention (Next.js has deprecated middleware.ts and renamed it proxy.ts), encode the result into a signed code using a FLAGS_SECRET, and rewrite the request to a route such as app/[code]/page.tsx. Each combination of flag values becomes a statically generated page, served through ISR or generated at build time.

The trade-offs are documented. Every flag multiplies the number of static permutations, so many flags or many values mean longer builds or lower cache hit rates, and ISR URLs must stay under 1,024 characters. Proxy defaults to the Node.js runtime. During React Server Components requests, Next.js strips internal Flight headers in Proxy; NextResponse.rewrite() carries the needed headers through, while custom fetch-based rewrites may not. Cost depends on the flag provider you choose, so check each provider's own pricing.

Choose this route for experiments on logged-in product flows, pricing logic, feature rollouts and anything that must render with no hidden period. Otter has no server-side SDK and no feature flags, so it doesn't replace this setup. For a closer look at that difference, read Otter vs LaunchDarkly.

How Next.js A/B testing works with Otter

Otter is a client-side tool. Its Next.js snippet goes in the <head> of app/layout.tsx, or pages/_document.tsx with the Pages Router. It adds three things: an inline style that hides the page, a reveal script that removes the style after 3 seconds as a failsafe, and the SDK loaded with next/script and strategy="afterInteractive". The SDK, about 32 KB gzipped, asks Otter for the visitor's test settings, applies the assigned variant and reveals the page. A returning visitor whose test can be served from cache sees it applied immediately.

Tests are then built outside your codebase. The visual editor opens your deployed site in a popup and changes text, HTML, attributes, styles and inserted content, so a marketer can test a pricing page headline without a pull request. Split URL tests send visitors to a second route, such as /pricing-b, through a browser redirect. Per-variant custom CSS and JavaScript cover what the editor can't.

For goals, call window.optimo.track('signup_completed') from your handlers; before the SDK has loaded, push calls onto window.optimoq and they replay once it boots. Revenue goes through window.optimo.revenue() with a value, currency and order ID. Otter can also send impressions and conversions to GA4 and the GTM dataLayer, or use GA4 events you already send as goals. See how Otter works for the full flow.

Client-side navigation in the App Router

Next.js changes routes without reloading the page, and a script loaded once in the root layout runs on the first page load only. Otter handles route changes itself. It detects Next.js from its _next/static scripts, patches history.pushState and replaceState, listens for popstate and hashchange, and polls the URL every 100 ms as a fallback. On each URL change it fetches settings again and re-evaluates which tests apply, and pageview goals re-check against the new URL.

Components that render after data loads need the right trigger. Use Wait for element to apply a test when a CSS selector appears, or DOM change to re-apply changes when React re-renders part of the page. For full control, use the Manual trigger and call window.optimo.execute(testId) from your component when it's ready. Otter also retries changes for 2 seconds on late-rendered elements, and re-applies "all instances" changes when matching elements appear.

Performance, flicker and Core Web Vitals

Otter hides the page until it has the visitor's test settings, with a 3-second failsafe, and it requests settings on every page that loads the snippet, including routes with no test. That stops visitors from seeing the original swap to a variant, but it adds a wait before the page becomes visible that depends on the round trip to Otter. Split URL tests always wait for that request, because redirect decisions are never served from the local cache. Measure your Largest Contentful Paint before and after installing.

Three ways to limit the cost. If only your marketing routes run tests, consider loading the snippet in that route group's layout so other routes don't wait on it. Keep variant routes fast, because a split URL test adds a navigation. And if a test must render with no hidden period at all, run it server-side with the Flags SDK. Strict Content Security Policies need attention too: allow Otter's domain for scripts and network requests, allow the inline anti-flicker style and reveal script with a nonce or hash, and note that per-variant custom JavaScript runs through new Function, so it needs 'unsafe-eval'.

What to test in a Next.js app

Next.js runs many SaaS marketing sites and product front ends, and the best first tests sit where visitors decide to sign up: the homepage headline and primary call to action, the pricing page's plan order and highlighted plan, and the first screen of signup. Because Otter follows route changes, a multi-step signup that never reloads can still be tested step by step, with a pageview goal on the final route or a custom event when the account is created.

Target tests to the audience that matters. Otter can split by UTM campaign, traffic source, device, country, new or returning visitors, cookies, URL parameters and your own visitor properties sent with window.optimo.identify(), such as plan or account type. Results can be broken down by the same dimensions afterwards. Pick a primary goal before launch and size the test with the sample size calculator.

Next.js A/B testing and SEO

Google's testing guidance says changing content with JavaScript is a valid way to test without new URLs, that redirects to a variant URL should be temporary (302) or JavaScript, and that alternate URLs should carry rel="canonical" pointing to the original. In the App Router, set a variant route's canonical with the Metadata API's alternates.canonical field. Don't cloak, and end tests once you have an answer.

Otter doesn't assign known bots to a variant, so crawlers see your original render. Server-side tests need the same care: if Proxy assigns variants by cookie, Google notes that Googlebot generally doesn't keep cookies, so it sees the version served to browsers without them, which should be your control.

Questions

Next.js A/B testing questions

How do I A/B test in Next.js?

Either server-side or client-side. Server-side, use Vercel's Flags SDK or a provider such as Statsig, PostHog, GrowthBook or LaunchDarkly to decide the variant in Proxy or on the server and render only that version. Client-side, add a testing script such as Otter to app/layout.tsx and build variants in its visual editor. Many teams use flags for product features and a client-side tool for marketing pages.

Does Otter work with the Next.js App Router?

Yes. Add Otter's Next.js snippet to the head of app/layout.tsx; it loads the SDK with next/script and strategy afterInteractive, plus an anti-flicker style. Otter detects Next.js, patches the History API and re-evaluates tests on every client-side route change, so tests and pageview goals work across navigations without full page reloads. Pages Router apps use pages/_document.tsx instead.

Does Otter have a server-side SDK for Next.js?

No. Otter is a client-side tool: tests apply in the browser, and split URL redirects happen in the browser too. It has no server-side SDK and no feature flags. For experiments that must be decided on the server, such as pricing logic or logged-in product flows, use Vercel's Flags SDK or a flag provider, and use Otter for marketing pages.

Will A/B testing cause flicker in Next.js?

Client-side testing has to manage it. Otter hides the page until the visitor's test settings load, with a 3-second failsafe, so visitors don't see the original swap to a variant, but the page waits for that request. Returning visitors with cached tests see them applied immediately. Server-side tests with the Flags SDK avoid the wait because only the chosen version is rendered.

How do I track conversions in a Next.js app?

Call window.optimo.track with an event name from your event handler, for example after a successful signup, and create a custom event goal with the same name; matching is exact and case-sensitive. Use window.optimo.revenue with a value, currency and order ID for purchases. Pageview goals on a route and click goals on a CSS selector work too, and GA4 events can be imported as goals.

Does A/B testing affect SEO in Next.js?

Not if you follow Google's guidance: change content with JavaScript or use temporary 302 or JavaScript redirects, add a canonical tag on variant routes pointing to the original, don't show crawlers different content from users, and end tests promptly. In the App Router, set canonicals with the Metadata API. Otter doesn't assign known bots to a variant, so crawlers see your original render.

Can I use Otter with a strict Content Security Policy?

Yes, with some adjustments. Allow Otter's domain for scripts and network requests so the SDK can load and fetch test settings, and allow the inline anti-flicker style and reveal script with a nonce or hash. Per-variant custom JavaScript runs through new Function, so it needs 'unsafe-eval'; if your policy forbids that, build variants with the visual editor and custom CSS instead.

Built for Next.js

Run your first test this afternoon

One snippet, no build step, and results you can act on within days.

  • 14-day free trial
  • No credit card required
  • Cancel anytime