Edge mode execution

Developer preview

This feature is in developer preview. Use with caution as it may change unexpectedly. For more information, contact us.

Developer Preview

Edge mode is new in the developer preview of the SDK, version 20.81.1-alpha.25.sha-f076f9b857. The APIs on this page can change before the stable release.

A Uniform page can contain personalizations and A/B tests, so different visitors can see different variants. Next.js serves pages fastest from the cache, but a cache entry is the same HTML for all visitors. Edge mode keeps one cached page for each route. The edge chooses the personalization and A/B test variants of the visitor before the browser gets the page. Thus the first paint is correct, and the page does not flicker.

In the developer preview, uniformMiddleware makes no API calls and does not choose variants. All visitors of a published route share one cached page. The SDK chooses the variants in one of two modes.

Use uniformMiddleware from @uniformdev/next-app-router/middleware.

The server renders the default variant of each placement:

  • Personalization: the variants that an anonymous visitor gets.
  • A/B test: the winner variant, else the control variant, else the first variant.

After hydration, the browser Context chooses the variants of the visitor. If they are not the defaults, the visitor sees the default first, and then the change. This change is the "flicker".

Lite mode is the least expensive setup, and it uses no more packages.

Use vercelUniformEdgeMiddleware from @uniformdev/next-app-router/vercel, or uniformEdgeMiddleware from @uniformdev/next-app-router/edge.

The cached page contains all variants. The edge middleware keeps only the variants of the visitor as the HTML goes to the browser. The visitor sees the correct variant on the first paint, with no flicker.

  1. The middleware puts the page in edge mode. The edge route filter selects published pages. For these pages, the middleware sets the edgeMode flag in the code. Draft and editor requests never use edge mode.
  2. The page renders all variants. With the edgeMode flag, the SDK renders each $personalization and $test component as an EdgePlacement. EdgePlacement puts each variant between NESI tags. Next.js caches this page one time for each route.
  3. The middleware fetches the cached page. For an HTML document request, the middleware fetches the rewritten URL with the x-uniform-edge-origin: 1 header. It makes the Context of the visitor from the cookies, the URL and the geolocation quirks at the same time.
  4. The middleware keeps the variants of the visitor. The middleware streams the HTML through a transform. The transform keeps the variants of the visitor and removes the other variants. When the response has no ETag, it also writes the state of the visitor into the __UNIFORM_DATA__ script for the browser Context.
  5. The browser hydrates the kept variants. EdgePlacement finds the variants that the edge kept, and hydrates them. It also sends the personalization and test events to the browser Context for analytics.

UniformContext adds the __UNIFORM_DATA__ script on pages in edge mode. You do not add it to your layout.

The middleware fetches and transforms only a GET request for an HTML document of a published page. These requests only get a rewrite:

  • Client-side navigations and prefetches. Next.js sends them with the rsc or next-router-prefetch header. The browser sends them with Sec-Fetch-Dest: empty.
  • Draft and editor requests. The browser chooses the variants.
  • Pages that the edge route filter skips. These pages stay cacheable by the CDN, and the browser chooses their variants.
  • Requests that are not GET requests.

The middleware finds a document request from the Sec-Fetch-Dest header: document, or iframe for the Canvas preview. Some clients, for example crawlers, do not send this header. For these clients, the Accept header must be missing, or must include text/html or */*.

A client-side navigation does not go through the transform. The RSC payload contains all variants, so EdgePlacement chooses the variants with the browser Context before the new page paints. Thus client-side navigations also show no flicker.

When the scores or quirks of the visitor change, EdgePlacement chooses the personalization variants again. Test variants do not change.

  1. Install the Vercel functions package:

    npm install @vercel/functions
  2. Replace the middleware:

    middleware.ts

    import { vercelUniformEdgeMiddleware } from "@uniformdev/next-app-router/vercel"; export default vercelUniformEdgeMiddleware({ // the same options as uniformMiddleware, for example rewriteRequestPath }); export const config = { matcher: [ { source: "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", missing: [{ type: "header", key: "x-uniform-edge-origin" }], }, ], runtime: "experimental-edge", };

    The middleware fetches the cached page with the x-uniform-edge-origin header. The missing rule stops a second run of the middleware on that fetch.

  3. Expire the Vercel runtime cache when content is published:

    app/api/preview/route.ts

    import { createPreviewGETRouteHandler, createPreviewOPTIONSRouteHandler, createPreviewPOSTRouteHandler, } from "@uniformdev/next-app-router/handler"; import { expireVercelRuntimeCacheTags } from "@uniformdev/next-app-router/vercel"; export const GET = createPreviewGETRouteHandler(); export const POST = createPreviewPOSTRouteHandler({ onRevalidateTags: expireVercelRuntimeCacheTags }); export const OPTIONS = createPreviewOPTIONSRouteHandler();

    Add a Uniform webhook for /api/preview. Refer to Caching.

  4. If you prebuild pages, prebuild the two edge mode values:

    app/uniform/[code]/page.tsx

    export const generateStaticParams = () => createUniformStaticParams({ paths: ["/"], edgeMode: [true, false] });

    The default filter skips pages that it learns have no placements. These pages get a code without the edge mode flag.

vercelUniformEdgeMiddleware uses these defaults:

OptionDefaultEffect
manifestvercelManifestProvider()Gets the published manifest at runtime, and keeps it in the Vercel runtime cache with the manifest tag.
filterlearnedEdgeRouteFilterSkips the pages that the store recorded without placements.
storevercelEdgeRouteStore()Records what the transform learns about each page in the Vercel runtime cache, with the path: tag of the page.
etagstrueGives per-visitor ETags to processed pages.

Set filter: false to process all published pages. Set store: false to record nothing. Then the edge processes all published pages, and responses get no ETag.

note

Under next dev, the Vercel helpers do not use the runtime cache, unless RUNTIME_CACHE_ENDPOINT is set. The manifest provider then gets the manifest from the API and keeps a copy in memory. The store records in memory for each instance, and the records do not expire. expireVercelRuntimeCacheTags does nothing. Restart next dev after you add a placement to a page that the store recorded without placements.

uniformEdgeMiddleware from @uniformdev/next-app-router/edge works without Vercel. You must give it the manifest:

middleware.ts

import type { ManifestV2 } from "@uniformdev/context"; import { uniformEdgeMiddleware } from "@uniformdev/next-app-router/edge"; import manifest from "./lib/uniform/contextManifest.json"; export default uniformEdgeMiddleware({ manifest: manifest as ManifestV2 }); export const config = { matcher: [ { source: "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", missing: [{ type: "header", key: "x-uniform-edge-origin" }], }, ], runtime: "experimental-edge", };

Download the manifest before each build:

package.json

{ "scripts": { "build": "npm run uniform:manifest && next build", "uniform:manifest": "uniform context manifest download --output lib/uniform/contextManifest.json" } }

With a manifest in the build, the middleware makes no network calls of its own. It only fetches the cached page.

warning

A manifest in the build becomes old when you publish new signals or tests. The edge uses them after the next build. Until then, the edge shows the defaults for new personalizations, and the browser chooses the variants of new tests.

uniformEdgeMiddleware accepts all middleware options, and these options:

OptionDefaultDescription
manifestRequiredThe Context manifest (ManifestV2), or a provider function that returns it.
filterProcess all published pagesAn EdgeRouteFilter that selects the pages to process.
storeRecord nothingAn EdgeRouteStore that records what the transform learns about each page.
etagstrueGives per-visitor ETags to processed pages. Works only with a store.
onErrorLog transform errors to the console. Manifest errors are not logged.Called when the manifest provider or the transform fails.

handleUniformEdgeRoute handles one request, so you can add your own logic around it. Give it waitUntil, so the middleware can finish cache writes after the response:

middleware.ts

import type { ManifestV2 } from "@uniformdev/context"; import { handleUniformEdgeRoute } from "@uniformdev/next-app-router/edge"; import type { NextFetchEvent, NextRequest } from "next/server"; import manifest from "./lib/uniform/contextManifest.json"; export default function middleware(request: NextRequest, event: NextFetchEvent) { return handleUniformEdgeRoute({ request, waitUntil: (promise) => event.waitUntil(promise), manifest: manifest as ManifestV2, }); }

createCachedManifestProvider gets the published manifest at runtime. Give it a cache with get and set functions for your platform:

import { createCachedManifestProvider, uniformEdgeMiddleware } from "@uniformdev/next-app-router/edge"; export default uniformEdgeMiddleware({ manifest: createCachedManifestProvider({ cache: myPlatformCache, // { get(key), set(key, value, { ttl, tags }) } }), });
OptionDefaultDescription
cachenoneA shared cache. Without it, each instance keeps the manifest in memory, and gets it again from the Uniform API after maxAge.
maxAge10The time in seconds that an instance uses its copy in memory before it reads the shared cache again.
ttlfalseThe time in seconds that the shared cache keeps the manifest. false keeps it until the manifest tag expires.
fallbacknoneA manifest to use when the first load fails.

The provider writes the manifest with the tag manifest. When a load fails, it uses the last manifest that it loaded, then the fallback. If neither exists, the middleware calls onError and uses an empty manifest. Then personalizations show their defaults, and the browser chooses the test variants.

An edge route filter decides which published pages the middleware processes. The middleware only rewrites a page that the filter skips. Such a page stays cacheable by the CDN, and the browser chooses its variants.

FilterBehavior
No filter (uniformEdgeMiddleware default)Process all published pages.
learnedEdgeRouteFilter (vercelUniformEdgeMiddleware default)Process a page until the store records that the page has no placements. Then skip it.
createStaticEdgeRouteFilter({ paths })Process only the listed paths. :name matches one path segment.
Your filterAn object with a shouldProcess function.
import { createStaticEdgeRouteFilter } from "@uniformdev/next-app-router/edge"; import { vercelUniformEdgeMiddleware } from "@uniformdev/next-app-router/vercel"; export default vercelUniformEdgeMiddleware({ filter: createStaticEdgeRouteFilter({ paths: ["/", "/campaigns/:slug"] }), });

A custom filter gets the page state and a function that reads the store record:

import type { EdgeRouteFilter } from "@uniformdev/next-app-router/edge"; const filter: EdgeRouteFilter = { shouldProcess: async ({ pageState, getRecord }) => !pageState.routePath.startsWith("/docs") && ((await getRecord())?.hasPlacements ?? true), };

Give the same filter to createUniformStaticParams({ edgeMode: filter }), so the prebuilt codes are the same as the codes from the middleware. At build time there is no store, so learnedEdgeRouteFilter selects all pages. Thus use edgeMode: [true, false] with the learned filter.

warning

The store does not know when a page gets a placement through a pattern or an entry. Example: you add a personalization to a pattern. A page that uses the pattern can stay skipped until its record expires.

To process the page again, publish the composition of the page, or change its project map node. This expires the path: tag of the record, but only for a static node path. The webhook for a dynamic node, for example /products/:slug, expires only path:/products. The records of the pages under that node stay until the cache removes them. For these pages, set a ttl in vercelEdgeRouteStore({ ttl }), or use a filter that does not skip them.

With a store and etags: true, the middleware gives a weak ETag to each processed page. The ETag comes from the page version and the variants of the visitor. When the browser sends the same ETag again, the middleware answers 304 Not Modified, and does not send the page again.

ResponseCache-ControlETag
The visitor state did not changeprivate, no-cacheYes
The visit changed the visitor state, for example a new scoreprivate, no-storeNo
No store, etags: false, the origin status is not 200, or the store has no record for this page version or for the variants of the visitorprivate, no-storeNo

The middleware sends the new visitor state without an ETag. A CDN can answer 304 for an ETag that it knows, and then the browser does not get the new state.

The origin page stays cached. Only the personalized response is private.

Edge mode uses the published Context manifest to choose the variants.

SetupWhere the manifest comes fromWhen it updates
vercelUniformEdgeMiddlewareUniform API, kept in the Vercel runtime cacheWhen the manifest.published webhook expires the manifest tag. Each instance keeps its copy in memory for a maximum of maxAge (10 seconds) more.
uniformEdgeMiddleware with createCachedManifestProviderUniform API, kept in your cacheWhen your cache expires the manifest tag, or after ttl
uniformEdgeMiddleware with a JSON fileThe buildAfter the next build

When the manifest of the edge does not know a signal yet, the edge shows the default variants. When it does not know a test yet, the browser chooses the variant.

  • Visibility rules run in the browser. NESI tags do not support visibility rules. A component with visibility rules is not in the server HTML, and shows after hydration.
  • Redirects pass through. When the page answers with a redirect, the middleware sends the redirect to the visitor without the transform.
  • Draft content is not processed. In Canvas preview, the browser chooses the variants.
  • One more request for each page load. The middleware fetches the cached page from its own deployment. On a deployment, this request goes across the edge network.
  • The personalized response is not cacheable by the CDN. It is private. The page that the middleware fetches stays cached.
  • The RSC payload contains all variants. The browser uses them to choose on client-side navigations. The edge removes the other variants from the HTML, but not from the inline RSC payload.
  • CPU time for the transform. The middleware decodes and encodes the HTML of each processed page.
  • The manifest is not always current. With a manifest in the build, new signals and tests reach the edge on the next build. With a provider, they reach it when the cache expires.

Cache Components caches parts of a page, but each cache entry is still the same for all users of its key. The variant of a visitor depends on cookies, the URL and geolocation headers.

  • 'use cache' cannot read cookies(), headers() or searchParams. The cached output is the same for all visitors, so the browser must choose afterwards. This is lite mode.
  • Cookies or headers read at request time make a dynamic part of the page. The server renders it for each request, and the visitor sees the Suspense fallback until it streams in.
  • 'use cache: private' can read cookies and headers, but Next.js does not keep the result on the server. A page load still renders the dynamic part on the server.

The middleware is the last place that sees the cached page and the request. Edge mode decides there, before the browser paints.