Edge mode execution
Developer preview
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.
Lite mode and edge mode#
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.
Lite mode: the browser chooses#
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.
Edge mode: the edge chooses#
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.
How edge mode works#
- The middleware puts the page in edge mode. The edge route filter selects published pages. For these pages, the middleware sets the
edgeModeflag in the code. Draft and editor requests never use edge mode. - The page renders all variants. With the
edgeModeflag, the SDK renders each$personalizationand$testcomponent as anEdgePlacement.EdgePlacementputs each variant between NESI tags. Next.js caches this page one time for each route. - The middleware fetches the cached page. For an HTML document request, the middleware fetches the rewritten URL with the
x-uniform-edge-origin: 1header. It makes the Context of the visitor from the cookies, the URL and the geolocation quirks at the same time. - 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. - The browser hydrates the kept variants.
EdgePlacementfinds 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.
Which requests the edge processes#
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
rscornext-router-prefetchheader. The browser sends them withSec-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 */*.
Client-side navigations#
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.
Set up edge mode on Vercel#
Install the Vercel functions package:
npm install @vercel/functionsReplace 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-originheader. Themissingrule stops a second run of the middleware on that fetch.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.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:
| Option | Default | Effect |
|---|---|---|
manifest | vercelManifestProvider() | Gets the published manifest at runtime, and keeps it in the Vercel runtime cache with the manifest tag. |
filter | learnedEdgeRouteFilter | Skips the pages that the store recorded without placements. |
store | vercelEdgeRouteStore() | Records what the transform learns about each page in the Vercel runtime cache, with the path: tag of the page. |
etags | true | Gives 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.
Set up edge mode on other hosts#
uniformEdgeMiddleware from @uniformdev/next-app-router/edge works without Vercel. You must give it the manifest:
middleware.ts
Download the manifest before each build:
package.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 options#
uniformEdgeMiddleware accepts all middleware options, and these options:
| Option | Default | Description |
|---|---|---|
manifest | Required | The Context manifest (ManifestV2), or a provider function that returns it. |
filter | Process all published pages | An EdgeRouteFilter that selects the pages to process. |
store | Record nothing | An EdgeRouteStore that records what the transform learns about each page. |
etags | true | Gives per-visitor ETags to processed pages. Works only with a store. |
onError | Log transform errors to the console. Manifest errors are not logged. | Called when the manifest provider or the transform fails. |
handleUniformEdgeRoute#
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
Load the manifest at runtime on other hosts#
createCachedManifestProvider gets the published manifest at runtime. Give it a cache with get and set functions for your platform:
| Option | Default | Description |
|---|---|---|
cache | none | A shared cache. Without it, each instance keeps the manifest in memory, and gets it again from the Uniform API after maxAge. |
maxAge | 10 | The time in seconds that an instance uses its copy in memory before it reads the shared cache again. |
ttl | false | The time in seconds that the shared cache keeps the manifest. false keeps it until the manifest tag expires. |
fallback | none | A 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.
Choose the pages for the edge#
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.
| Filter | Behavior |
|---|---|
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 filter | An object with a shouldProcess function. |
A custom filter gets the page state and a function that reads the store record:
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.
Per-visitor ETags#
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.
| Response | Cache-Control | ETag |
|---|---|---|
| The visitor state did not change | private, no-cache | Yes |
| The visit changed the visitor state, for example a new score | private, no-store | No |
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 visitor | private, no-store | No |
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.
The Context manifest#
Edge mode uses the published Context manifest to choose the variants.
| Setup | Where the manifest comes from | When it updates |
|---|---|---|
vercelUniformEdgeMiddleware | Uniform API, kept in the Vercel runtime cache | When 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 createCachedManifestProvider | Uniform API, kept in your cache | When your cache expires the manifest tag, or after ttl |
uniformEdgeMiddleware with a JSON file | The build | After 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.
Limits#
- 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.
What edge mode costs#
- 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.
Why Cache Components does not replace edge mode#
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 readcookies(),headers()orsearchParams. 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
Suspensefallback 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.