
A/B testing on Netlify without flicker using edge functions
Why A/B testing is harder than it looks on a static site
A/B testing is a simple idea: show two versions of a page (or flow) to different users and measure which one performs better. Your "better" might mean more signups, more clicks, or more purchases.
The annoying part is how A/B testing is often implemented.
A lot of tools run in the browser using client-side JavaScript. The page loads, then the tool swaps content. This can cause a flash of the original version before the experiment version shows up. It can also slow down the page, and that can mess with your results.
If your site is on Netlify, you have a nicer option: run the split test at the edge with Netlify Edge Functions. That means the decision happens before HTML reaches the browser.
A/B testing with Netlify Edge Functions
With Netlify Edge Functions A/B testing, you typically do two things:
- Assign each visitor to a bucket (A or B) using a cookie.
- Rewrite the request based on that cookie so they get the correct version from the same public URL.
This has some big benefits:
- No flicker in the browser because the page is selected before rendering starts
- Works with statically generated pages because Netlify can rewrite the request at runtime
- Your test logic lives in your repo with version control
- Users keep seeing the same variant on future page views (variant affinity)
Example: split testing product page layouts
Let's say your product URLs look like this:
/product/{productId}/
You have:
- The existing product page layout at
/product/{productId}/ - A new layout you want to test at
/product-v2/{productId}/
The goal is:
- Visitors still go to
/product/{productId}/ - Netlify Edge Functions silently rewrites some users to
/product-v2/{productId}/
Step 1: Create the Edge Function file
Create:
netlify/edge-functions/product-layout-test.ts
Folder structure:
.
└── netlify
└── edge-functions
└── product-layout-test.ts
Step 2: Configure the route in netlify.toml
Add this in your project root netlify.toml:
[[edge_functions]]
function = "product-layout-test"
path = "/product/*"
This tells Netlify to run the edge function for any /product/... request.
Step 3: Assign users to buckets with a cookie
Here is a complete TypeScript Edge Function that:
- Reads the cookie
product_layout_test - If missing, assigns a bucket (A or B)
- Rewrites requests to the new layout when the user is in bucket B
import type { Config, Context } from "https://edge.netlify.com"
type Variant = "A" | "B"
const COOKIE_NAME = "product_layout_test"
// Change this if you want something other than 50/50.
// Example: 0.2 means 20% see variant B, 80% see variant A.
const VARIANT_B_RATIO = 0.5
function pickVariant(): Variant {
return Math.random() < VARIANT_B_RATIO ? "B" : "A"
}
function getProductId(pathname: string): string | null {
// Expected: /product/{productId}/
const parts = pathname.split("/").filter(Boolean)
if (parts.length < 2) return null
if (parts[0] !== "product") return null
return parts[1]
}
export default async (request: Request, context: Context) => {
const url = new URL(request.url)
const productId = getProductId(url.pathname)
// If the URL is not what we expect, do nothing
if (!productId) return
const existing = context.cookies.get(COOKIE_NAME)
let variant: Variant
if (existing === "A" || existing === "B") {
variant = existing
} else {
variant = pickVariant()
context.cookies.set({
name: COOKIE_NAME,
value: variant,
path: "/",
// keep it sticky for a while so visitors stay in the same bucket
maxAge: 60 * 60 * 24 * 30 // 30 days
})
}
// Variant A: keep the current experience and continue normally
if (variant === "A") return
// Variant B: rewrite to the new layout, but keep the public URL unchanged
return new URL(`/product-v2/${productId}/`, request.url)
}
export const config: Config = {
path: "/product/*"
}
What this does:
- First-time visitors get randomly assigned to A or B.
- A cookie makes sure they keep seeing the same layout later.
- Variant B users get served the new layout from
/product-v2/...while the browser still shows/product/....
Tracking which variant a user saw
Splitting traffic is only half the job. You also need to track it.
One practical approach is to read the cookie in the browser and attach the variant to analytics events. This example shows a simple way to read the cookie value.
If you already have an analytics setup, you can adapt this to your tool.
function readCookie(name: string): string | null {
const all = document.cookie.split(";").map((c) => c.trim())
const found = all.find((row) => row.startsWith(`${name}=`))
if (!found) return null
return found.split("=")[1] ?? null
}
const variant = readCookie("product_layout_test") ?? "unknown"
// Example: send the variant as metadata to your analytics tool
// Replace this with your actual tracking call
window.dispatchEvent(
new CustomEvent("ab_test_variant", {
detail: { test: "product_layout_test", variant }
})
)
This keeps the A/B assignment logic on Netlify, but still gives your analytics a way to know which layout a visitor saw.
Notes and common gotchas
- Geolocation and other edge-only features might behave differently locally. For basic rewrites and cookies, local testing is still useful.
- If you use Next.js, your routing and middleware may be different. Netlify has specific approaches for Next.js edge middleware, so follow Netlify's Next.js guidance if you are in that world.
- Keep your variant URLs real pages. The rewrite is cleanest when both variants are already built and deployed.
Conclusion
If you want A/B testing on Netlify without slowing down the page or causing flicker, Netlify Edge Functions split testing is a strong approach.
You:
- Assign a user to a bucket with a cookie
- Rewrite requests at the edge based on that cookie
- Keep the public URL the same
- Track the variant in your analytics setup
It is a small amount of code, but it gives you a fast and controlled way to test layouts and user journeys on a Netlify hosted site.
Manage Netlify on the go
Download Netli.fyi and monitor your sites, check deploys, and manage your projects from anywhere.


