惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Apple Machine Learning Research
Apple Machine Learning Research
aimingoo的专栏
aimingoo的专栏
H
Help Net Security
腾讯CDC
T
Tailwind CSS Blog
Hugging Face - Blog
Hugging Face - Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
MongoDB | Blog
MongoDB | Blog
宝玉的分享
宝玉的分享
有赞技术团队
有赞技术团队
美团技术团队
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 司徒正美
博客园_首页
Recent Announcements
Recent Announcements
云风的 BLOG
云风的 BLOG
B
Blog RSS Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
D
Docker
博客园 - Franky
Jina AI
Jina AI

Vercel News

Vercel Open Source Program: Winter 2026 cohort How Notion Workers run untrusted code at scale with Vercel Sandbox How we run Vercel's CDN in front of Discourse From idea to secure checkout in minutes with Stripe Building Slack agents can be easy Scaling redirects to infinity on Vercel Advancing Python typing Gamma builds design-first agents with Vercel How Avalara turns pipe dreams into patent-pending with v0 Keeping community human while scaling with agents How OpenEvidence built a healthcare AI that physicians actually trust Security boundaries in agentic architectures Skills Night: 69,000+ ways agents are getting smarter Video Generation with AI Gateway We Ralph Wiggumed WebStreams to make them 10x faster How Stably ships AI testing agents in hours, not weeks How we built AEO tracking for coding agents Anyone can build agents, but it takes a platform to run them Introducing Geist Pixel The Vercel AI Accelerator is back with $6m in credits Making agent-friendly pages with content negotiation The Vercel OSS Bug Bounty program is now available Introducing the new v0 Run untrusted code with Vercel Sandbox, now generally available How Stripe built a game-changing app in a single flight with v0 How Sensay went from zero to product in six weeks AGENTS.md outperforms skills in our agent evals Agent skills explained: An FAQ Testing if "bash is all you need" AWS databases are now live on the Vercel Marketplace and v0
How to run A/B tests with Next.js and Vercel - Vercel – V...
Steven Tey · 2022-09-09 · via Vercel News

4 min read

Running A/B tests is hard.

We all know how important it is for our business–it helps us understand how users are interacting with our products in the real world.

However, a lot of the A/B testing solutions are done on the client side, which introduces layout shift as variants are dynamically injected after the initial page load. This negatively impacts your websites performance and creates a subpar user experience.

To get the best of both worlds, we built Edge Middleware: code that runs before serving requests from the edge cache. This enables developers to perform rewrites at the edge to show different variants of the same page to different users.

Today, we'll take a look at a real-world example of how we used Edge Middleware to A/B test our new Templates page.

Link to headingRedesigning the Template Marketplace

Earlier this year, we kickstarted the redesign of our Templates page.

Our old Templates page had a limited scope that focused mainly on the different frameworks that were supported on Vercel:

Old version: limited selection of templates that focused mainly on framework starters.Old version: limited selection of templates that focused mainly on framework starters.

Old version: limited selection of templates that focused mainly on framework starters.

For the new version, we wanted to build a marketplace to showcase all the different types of applications that can be built on Vercel – blogs, e-commerce storefronts, etc.

We also improved search to allow for partial matching of search terms and added category filters to make discovering new templates much easier:

New version: wider selection of templates with fuzzy search & filters.New version: wider selection of templates with fuzzy search & filters.

New version: wider selection of templates with fuzzy search & filters.

Since this was a major change of one of our most popular pages, we decided to break down the launch into multiple release phases:

  1. Early Access: Only 20% of visitors to /templates will see the new variant.

  2. Public Beta: An equal 50:50 split between the new and old variant.

  3. General Availability: Public launch–everyone gets the new variant.

We used Vercel's Edge Middleware to stagger our launch into 3 phases.We used Vercel's Edge Middleware to stagger our launch into 3 phases.

We used Vercel's Edge Middleware to stagger our launch into 3 phases.

We leveraged Edge Middleware to perform the A/B test at the edge, which allowed us to gather insightful user feedback without sacrificing performance.

Link to headingCatch-all Routes & On-demand Revalidation

Since we were using Next.js, we decided to create the new templates marketplace using the following optional catch-all route: /templates/[[...slug]].tsx.

This allowed us to programmatically generate new static pages when new templates are added using On-Demand Incremental Static Regeneration (ISR) and navigate between them using shallow-routing.

Here are some examples:

We also generated dynamic landing pages for each filter option to capture long-tail SEO traffic:

We then moved the old templates marketplace to the /templates-old.tsx route.

Link to headingExperimenting at the Edge

To perform the A/B test, we created a middleware.ts file with the following code:

middleware.ts

import { type NextRequest, NextResponse } from 'next/server';

// make sure the middleware only runs when

// the requested url starts with `/templates`

export const config = {

matcher: ['/templates(.*)'],

};

const THRESHOLD = 0.2; // initial threshold for the new variant (20%)

const COOKIE_NAME = 'tm_var'; // name of the cookie to store the variant

export function middleware(req: NextRequest) {

// get the variant from the cookie

// if not found, randomly set a variant based on threshold

const variant =

req.cookies.get(COOKIE_NAME) || (Math.random() < THRESHOLD ? 'new' : 'old');

const url = req.nextUrl.clone();

// if it's the old variant, rewrite to the old templates marketplace

if (variant === 'old') {

url.pathname = '/templates-old';

}

const res = NextResponse.rewrite(url);

// set the variant in the cookie if not already set

if (!req.cookies.get(COOKIE_NAME)) {

res.cookies.set(COOKIE_NAME, variant);

}

return res;

}

Middleware code to perform the A/B test at the edge.

Here's a step by step of what the middleware does:

  1. Tags each new visitor to the /templates page with either the old or new variant based on the current threshold (0.2 for the first stage)

  2. If the user is part of the old variant, performs a rewrite to the /templates-old.tsx route

  3. Saves the user's variant in the tm_var cookie to ensure the user gets served the same version in subsequent visits

Link to headingAvoiding layout shift with Edge Middleware

As we developed the new templates page, we ran into an interesting problem–data fetching had to be done on the client side for three reasons:

  1. We were using query parameters to preserve the filter state

  2. The pages were generated statically

  3. You can't read query parameters inside getStaticProps

To avoid layout shift, we decided to use skeleton loaders to buffer the loading state before showing the templates that match the configured filters.

For instance, if you go to vercel.com/templates?framework=svelte, you first receive a set of skeleton loaders while the data is fetched on the client side, and then the list of Svelte templates:

However, for the dynamic landing pages for each filter option (e.g. vercel.com/templates/svelte), we wanted to avoid skeleton loaders since we already know the set of templates that match the given filter at build time.

To get the best of both worlds, we used Edge Middleware. Since Edge Middleware is executed before a request is processed on a site, we are able to detect if there are any query parameters present right when the user requests the page.

Then, if there are query parameters present, we perform a rewrite to /templates/skeleton, which is a special route that shows a skeleton loader.

Here's the middleware code from earlier, with the addition of this logic:

middleware.ts

import { type NextRequest, NextResponse } from 'next/server';

// make sure the middleware only runs when

// the requested url starts with `/templates`

export const config = {

matcher: ['/templates(.*)'],

};

const THRESHOLD = 0.2; // initial threshold for the new variant (20%)

const COOKIE_NAME = 'tm_var'; // name of the cookie to store the variant

export function middleware(req: NextRequest) {

// get the variant from the cookie

// if not found, randomly set a variant based on threshold

const variant =

req.cookies.get(COOKIE_NAME) || (Math.random() < THRESHOLD ? 'new' : 'old');

const url = req.nextUrl.clone();

// if it's the old variant, rewrite to the old templates marketplace

if (variant === 'old') {

url.pathname = '/templates-old';

} else {

// for the new variant, we need to perform a rewrite to /template/skeleton

// if there are query paramters in the request URL

const hasQueryParams = req.nextUrl.search.length > 0;

if (hasQueryParams) {

url.pathname = `/templates/skeleton`;

}

}

const res = NextResponse.rewrite(url);

// set the variant in the cookie if not already set

if (!req.cookies.get(COOKIE_NAME)) {

res.cookies.set(COOKIE_NAME, variant);

}

return res;

}

Middleware code to perform an edge rewrite to avoid layout shift.

Then, on the client side, we detect when the data is finished fetching with SWR and render the final list of cards:

templates/[[...slug]].tsx

import useSWR from "swr"

import { useRouter } from "next/router"

import { Card, PlaceholderCard, CardProps } from '@/components/cards'

export default function Templates({ skeleton } : { skeleton: boolean }) {

const router = useRouter()

const { data } = useSWR<CardProps[]>("/api/templates")

// if skeleton is true and the router is still loading, show a skeleton

if (!data || (skeleton && !router.isReady)) {

return (

<div>

{[...Array(6).keys()].map((i: number) => (

<PlaceholderCard key={i}/>

))}

</div>

)

}

return (

<div>

{data.map((d: CardProps, i) => (

<Card key={i} data={d} />

))}

</div>

)

}

export function getStaticProps(context) {

const skeleton = context.params?.slug?.includes('skeleton')

return {

props: {

skeleton,

},

}

}

Simplified version of the code that shows how we use SWR and middleware to conditionally render loader skeletons on the client side if there are query parameters present in the request URL.

Link to headingCollecting data & iterating

Throughout our Early Access and Public Beta phases, we collected a series of data points that helped us understand how our new Templates Marketplace was performing in the real world.

Link to headingTracking conversion rates with Heap

We used Heap to track our conversion rates: how likely is a user to successfully deploy a template after seeing it on the new vs. old Templates pages.

Based on the data from Heap, we were able to make some tweaks to our implementation to increase conversion rates by 16% with the new variant.

Link to headingTracking search & filter usage Stats with Algolia

Algolia's Search Without Results feature was also instrumental for us to understand which templates users were interested in. Based on the data, we were getting >30 searches a week for WordPress templates that yielded no results.

This prompted us to add a few WordPress templates before releasing the Templates Marketplace to General Availability.

Link to headingEdge Middleware: Run A/B tests without the tradeoffs

Edge Middleware allowed us to streamline the release of our new Templates Marketplace by putting the new version behind a feature flag until it's fully tested and optimized.

Check out our Templates Marketplace today to jumpstart your app development process with our pre-built solutions.