Use cases
Next.js A/B testing without flicker
Flicker comes from one sequence: render the control, then mutate the DOM after a script loads. Apertio removes the sequence by resolving each variant server-side, so the assigned variant is in the HTML on first paint.
Flicker comes from one specific sequence. A client-side testing tool ships the page with its control content, then mutates the DOM after its script loads and runs. For a moment the visitor sees the original. To hide that moment, these tools install an anti-flicker snippet that blanks the page until the library finishes. You trade a flash of the wrong content for a flash of nothing, and you pay for it in Largest Contentful Paint.
Apertio removes the sequence. Variants resolve server-side or at the edge and render through your app’s own React components. The resolved value arrives as props before the first render of the instrumented component, so there is no control state to flash away from and no snippet to hide. The question that decides flicker is whether the resolved slot map is present before first render. Under server-side rendering in Next.js the answer is always yes.
A typed slot, not a code branch
Apertio’s unit of change is a typed slot, not a code branch and not a DOM patch. An engineer declares a slot once: a headline with a maximum length, or an enum that picks one of several prebuilt layouts. Marketing fills and tests those slots within their declared bounds with no new code. The variant payload is data, a string or an enum key, validated against the in-code schema. Your components turn that data into pixels. Apertio never touches the DOM.
Resolution runs in three stages, and keeping them separate is what makes the no-flicker story hold across rendering modes. Assignment decides which variant a visitor gets, computed once from a deterministic hash of the visitor’s unit id and persisted in a cookie. Resolution turns that assignment plus the published config into a concrete map of slot keys to values. Render hands that map to your components. In a Server Component the first two stages finish before any HTML is streamed, so the assigned variant is in the server HTML on first paint.
A Server Component resolves before HTML ships
// app/page.tsx — a Server Component resolves the variant before HTML ships.
import { Suspense } from 'react'
import { resolveExperiments } from '@apertio/next/server'
import { ApertioProvider } from '@apertio/next/provider'
import { slots } from '@/apertio/slots'
import { configSource, eventSink, prewarm } from '@/apertio/runtime'
import { Hero } from '@/components/Hero'
// The slot map is resolved server-side, then passed to the provider as props.
// The assigned variant is in the server HTML on first paint: zero flicker.
async function ResolvedHero() {
await prewarm() // load the config snapshot once; steady-state resolve is sync
const result = await resolveExperiments({ slots, configSource, eventSink })
return (
<ApertioProvider states={result.states}>
<Hero />
</ApertioProvider>
)
}
export default function Page() {
return (
<Suspense fallback={null}>
<ResolvedHero />
</Suspense>
)
} // components/Hero.tsx — your own component reads the typed, resolved value.
'use client'
import { useSlot } from '@/apertio/hooks' // createApertio(slots), fully typed
export function Hero() {
// string, never undefined. No "before" state, so nothing flashes.
const headline = useSlot('home.hero.headline')
return <h1>{headline}</h1>
} The assigned variant is in the server HTML on first paint: zero flicker.
The config snapshot reaches the SDK as an HTTP JSON payload, cached in memory per instance and refreshed in the background with stale-while-revalidate through a ConfigSource interface. A pre-warm at startup loads it before first render, so the resolver behaves synchronously in steady state. Almost every request reads from memory, with no per-request network call.
Resolved before render
The slot map arrives as props before the instrumented component’s first render. There is no default-then-swap, so there is nothing to flicker.
No anti-flicker snippet
Nothing hides your page while a library loads. Apertio injects no third-party JavaScript and mutates no live DOM, so a strict nonce-based CSP with no unsafe-inline and no unsafe-eval stays intact.
Typed slots, your components
useSlot('home.hero.headline') returns a typed value resolved against your in-code schema. Your own React components render it, so the experiment matches your design system exactly.
The default is the loading state
Every slot carries a mandatory default, so the component is never blocked and never empty. If config is absent or the visitor is excluded, the default renders. No shimmer.
SSR is the gold path; PPR keeps the cache
Server-side rendering is the gold path: zero flicker, at the cost that the page is not fully cacheable, so you pay compute and latency per request. If you want the cache back, PPR / Cache Components is the recommended default. The static shell prerenders and caches, and the experimentable slots are the dynamic holes, so you get a cacheable page plus per-request resolved slots without making the whole page dynamic. The slot boundary is the prerender boundary.
The honest limitation: only modeled slots are testable. A novel surface still needs an engineer to declare a slot for it. That moves the engineering cost from one per test to one per new surface, and the aim is to make adding a surface a short PR.
Resolve your variants before the HTML ships
Apertio is open source and MIT licensed. Read the docs to instrument your first slot, or start free.
Frequently asked questions
- Does Apertio need an anti-flicker snippet?
- No. Anti-flicker snippets exist to hide a page while a client-side library mutates the DOM. Apertio resolves each variant server-side or at the edge and renders it through your own components, so the assigned variant is in the HTML on first paint. There is no flash to hide.
- Where does the variant get resolved in Next.js?
- In a Server Component, before any HTML is streamed. resolveExperiments reads the assignment cookie, loads the config snapshot, and produces the resolved slot map. That map is passed to the provider as props, so the value is present before the instrumented component’s first render.
- Does this work with PPR and Cache Components?
- Yes, and that is the recommended default. The static shell prerenders and caches while the experimentable slots are the dynamic holes. You keep a cacheable page and still resolve each slot per request, so there is no flicker and no fully dynamic page.
- Will it slow my page down?
- The config snapshot is fetched once and cached in memory per instance, refreshed in the background with stale-while-revalidate. A pre-warm loads it before first render, so almost every request resolves from memory with no per-request network call. Server-side rendering does cost compute and latency per request, which is the tradeoff for a non-cached page. PPR keeps the shell cached.