Concepts

Precompute

Use feature flags on static pages.

Precomputing describes a pattern where Edge Middleware uses feature flags to decide which variant of a page to show. This allows you to keep the page itself static, which leads to incredibly low latency globally as the page can be served by a CDN or Edge Network.

Precompute manual

With precompute, you can:

  • Combine multiple feature flags on a single, static page.
  • Use middleware to make routing decisions.
  • Generate pages for each flag combination at build time or lazily, the first time it's accessed.
  • Cache pages with Incremental Static Regeneration (ISR).

Precompute works by using dynamic route segments to transport an encoded version of the feature flags computed within Edge Middleware. Encoding the values within the URL allows the page itself to access the precomputed values, and also ensures there is a unique URL for each combination of feature flags on a page. Because the system works using rewrites, the visitor will never see the URL containing the flags. They will only see the clean, original URL.

Manual approach

It's possible to manually create variants of page by creating two versions of the same page. For example, app/home-a/page.tsx and app/home-b/page.tsx. Then, use Edge Middleware to rewrite the request either to /home-a or /home-b.

flags.tsx
import { flag } from 'flags/next';
 
export const homeFlag = flag<boolean>({
  key: 'home',
  decide: () => Math.random() > 0.5,
});
middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { homeFlag } from './flags.ts';
 
export const config = { matcher: ['/'] };
 
export async function middleware(request: NextRequest) {
  const home = await homeFlag();
 
  // Determine which version to show based on the feature flag
  const version = home ? '/home-b' : '/home-a';
 
  // Rewrite the request to the appropriate version
  const nextUrl = new URL(version, request.url);
  return NextResponse.rewrite(nextUrl);
}

This approach works well for simple cases, but has a few downsides:

  • It can be cumbersome having to maintain both /home-a/page.tsx and /home-b/page.tsx.
  • It doesn't scale well when a feature flag is used on more than one page, or when multiple feature flags are used on a single page.

The precompute functionality of the Flags SDK can be used to work around these limitations.

Precompute pattern

1. Create flags to be precomputed

Export one or multiple flags as an array to be precomputed.

flags.tsx
import { flag } from 'flags/next';
 
export const showSummerSale = flag({
  key: 'summer-sale',
  decide: () => false,
});
 
export const showBanner = flag({
  key: 'banner',
  decide: () => false,
});
 
// a group of feature flags to be precomputed
export const marketingFlags = [showSummerSale, showBanner] as const;

2. Precompute flags in middleware

Import and pass the group of flags to the precompute function in middleware. Then, forward the precomputation result (code) to the underlying page using an URL rewrite:

middleware.ts
import { type NextRequest, NextResponse } from 'next/server';
import { precompute } from 'flags/next';
import { marketingFlags } from './flags';
 
// Note that we're running this middleware for / only, but
// you could extend it to further pages you're experimenting on
export const config = { matcher: ['/'] };
 
export async function middleware(request: NextRequest) {
  // precompute returns a string encoding each flag's returned value
  const code = await precompute(marketingFlags);
 
  // rewrites the request to include the precomputed code for this flag combination
  const nextUrl = new URL(
    `/${code}${request.nextUrl.pathname}${request.nextUrl.search}`,
    request.url,
  );
 
  return NextResponse.rewrite(nextUrl, { request });
}

3. Access the precomputation result from a page

Next, import the feature flags you created earlier, such as showBanner, while providing the code from the URL and the marketingFlags list of flags used in the precomputation.

When the showBanner flag is called within this component it reads the result from the precomputation, and it does not invoke the flag's decide function again:

app/[code]/page.tsx
import { marketingFlags, showSummerSale, showBanner } from '../../flags';
type Params = Promise<{ code: string }>;
 
export default async function Page({ params }: { params: Params }) {
  const { code } = await params;
  // access the precomputed result by passing params.code and the group of
  // flags used during precomputation of this route segment
  const summerSale = await showSummerSale(code, marketingFlags);
  const banner = await showBanner(code, marketingFlags);
 
  return (
    <div>
      {banner ? <p>welcome</p> : null}
 
      {summerSale ? (
        <p>summer sale live now</p>
      ) : (
        <p>summer sale starting soon</p>
      )}
    </div>
  );
}

This approach allows middleware to decide the value of feature flags and to pass the precomputation result down to the page. This approach also works with API Routes.

Enabling ISR (optional)

You can enable Incremental Static Regeneration (ISR) to cache generated pages after their initial render:

app/[code]/layout.tsx
import type { ReactNode } from 'react';
 
export async function generateStaticParams() {
  // returning an empty array is enough to enable ISR
  return [];
}
 
export default async function Layout({ children }: { children: ReactNode }) {
  return children;
}

In the example above, we used generateStaticParams on the layout. You can also specify it on the page instead. It depends on whether a single page needs the flag or all pages within that layout need the flag.

Opting into build-time rendering (optional)

The flags/next submodule exposes the generatePermutations helper function for generating pages for different combinations of flags at build time. This function is called and takes a list of flags and returns an array of strings representing each combination of flags:

app/[code]/page.tsx
import type { ReactNode } from 'react';
import { generatePermutations } from 'flags/next';
 
export async function generateStaticParams() {
  const codes = await generatePermutations(marketingFlags);
  return codes.map((code) => ({ code }));
}
 
export default function Page() {
  /* ... */
}

You can further customize which specific combinations you want render by passing a filter function as the second argument of generatePermutations. And just like in the example above, you can also control whether you specify these permutations on the individual pages or on a layout.

Pages Router

If you're using the Pages Router, you need to pass a flag to generatePermutations which accepts the code from context and the group of flags.

You also need to specify a getStaticPaths function which can return the permutations to generate at build time or an empty array to use ISR.

pages/precomputed/[code]/index.tsx
import { generatePermutations } from 'flags/next';
import { marketingFlags, exampleFlag } from '../flags';
 
export const getStaticPaths = (async () => {
  const codes = await generatePermutations(marketingFlags);
 
  return {
    paths: codes.map((code) => ({ params: { code } })),
    fallback: 'blocking',
  };
}) satisfies GetStaticPaths;
 
export const getStaticProps = (async (context) => {
  if (typeof context.params?.code !== 'string') return { notFound: true };
 
  const example = await exampleFlag(context.params.code, marketingFlags);
  return { props: { example } };
}) satisfies GetStaticProps<{ example: boolean }>;`}

Declaring available options (optional)

Options are the possible values that a flag can take. You can declare the available options for a flag by passing an options array to the flag function:

flags.ts
export const greetingFlag = flag<string>({
  key: 'greeting',
  options: ['Hello world', 'Hi', 'Hola'],
  decide: () => 'Hello world',
});

Instead of passing the values directly you can also pass an object containing a label and value property:

flags.ts
export const greetingFlag = flag<string>({
  key: 'greeting',
  options: [
    { label: 'Hello world', value: 'Hello world' },
    { label: 'Hi', value: 'Hi' },
    { label: 'Hola', value: 'Hola' },
  ],
  decide: () => 'Hello world',
});

To pass objects you must specify a label and value property:

flags.ts
export const greetingFlag = flag<string>({
  key: 'greeting',
  options: [
    {
      label: 'Hello world',
      value: {
        /* your object here */
      },
    },
  ],
});

The Flags SDK uses the declared options for multiple purposes:

  1. Efficiently encode the flag values into the URL

    The precompute function generates a short code which your appliciation then transports through the URL. The URLs must remain fairly short for the system to stay efficient. When a feature flag's decide function returns a value not explicitly declared in options the whole value needs to be inlined into the code which can quickly exceed the URL size limits. Especially for ISR the URL length needs to stay below 1024 characters.

  2. Generate the possible permutations flags

    The generatePermutations function generates all possible combinations of flags for prerendering at build time. The function needs to know the available options for each flag to generate the possible permutations. It can only generate and prerender options decalred by the flag.

  3. Show the available options in the Flags Explorer

    All options declared for a flag are shown in the Flags Explorer. If present, the label is used as the option name.

Next steps

See the Marketing Pages example which implements this pattern