Experiments that don't shift the layout
Open the network tab on a site running a popular client-side testing tool and you can watch the crime happen: the real hero renders, the tool's snippet boots, the hero is torn down and replaced with a variant, and the metrics record a layout shift the size of a billboard. Worse is the tool's recommended fix — an "anti-flicker" snippet that hides the entire page behind an opacity curtain for up to four seconds while it decides what you're allowed to see.
That is not experimentation infrastructure. That is a ransom note stapled to your Core Web Vitals. And it's unnecessary: with the variant decision made on the server, the page a visitor receives is born correct. No flicker, no shift, no curtain. Here is the architecture we run, the discipline it demands, and the honest ledger of what you give up.
Why client-side testing corrupts the experiment itself
The performance damage is the visible crime; the subtler one is that client-side testing measures a page that no longer exists. Three problems compound:
The treatment is confounded by its delivery. A variant that wins might be winning despite arriving 400ms late and shoving the page around. Ship the winner as real code — fast, stable, server-rendered — and the lift often evaporates, because what you measured was "new headline plus layout shift," and you've only kept the headline.
Bots and blockers eat your sample. Test snippets are among the most-blocked scripts on the web. The blocked cohort isn't random — it over-indexes on technical, privacy-conscious, high-intent users (your best customers, on a B2B site). Your experiment is quietly being run on the remainder.
The flicker is a brand experience. Visitors can see pages change under the cursor. On a pricing page, "the number moved" reads as bait-and-switch even when the intent was innocent — one reason we insist pricing experiments stay honest at the design level too.
Server-side assignment: the pattern
The architecture that eliminates all three problems at once is unglamorous:
- Assign at the edge, on request. A cookie (or signed session token) carries the visitor's experiment assignments. On the first experiment-bearing request, the edge layer draws the assignment — seeded by a stable user key so the same person gets the same variant forever — writes the cookie, and renders accordingly. Subsequent requests just read it.
- Render the variant on the server. The variant is code behind a flag, not a DOM mutation. The HTML that leaves the origin is the experiment. There is nothing to flicker.
- Report exposure from the server render. The page emits an exposure event at request time, into your own analytics pipeline. Assignment and exposure are indistinguishable from any other page view — no special client, nothing to block short of blocking the whole site.
On a statically prerendered site (like the ones we mostly build), step 1 happens at the CDN: the edge worker inspects the assignment cookie and serves one of the statically generated variants from cache, keyed by the cookie. The cache is warmed per variant, hit ratios stay healthy because variant count is small, and the origin barely notices experiments exist.
The implementation detail that makes or breaks cacheability: put the assignment on the cache key, not the cookie header at large. If you vary on the whole Cookie header you'll shard your cache into per-user gravel. Rewrite to a tiny x-exp header containing only active experiment IDs, and vary on that.
Flags that don't leak into the DOM
A server-side experiment is only flicker-free if the loser leaves no forensic evidence. The rules we enforce in review:
- One source of truth. The flag is read at request/render time and passed down as props. Components never re-read the flag client-side — a flag that can be read at two times can return two answers.
- No variant artifacts in the losing tree.
if (variant === 'b')belongs in render logic, not in CSS that renders both and hides one withdisplay: none. Hidden variants still cost bytes, still appear to assistive tech unless carefully excluded, and — the killer — still affect layout the day someone changes a stylesheet. - Reserve space through design, not measurement. Where a test legitimately differs in size (a taller hero, an extra trust badge row), the design system must already have a spacing token story for it, so both variants sit inside the same stable layout rhythm. If the variant fundamentally changes the above-fold geometry, that experiment should be run as a separate page template at the edge — still server-side, still shift-free — not as a component swap.
- Kill switches are data, not deploys. Every live experiment sits behind the same feature flag discipline as product features: remotely disableable in under a minute, owned by a named human, carrying an expiry date. Zombie experiments are how "temporary" test code becomes load-bearing.
Metric integrity, or: the experiment lies you can't see
Flicker-free rendering is worthless if the numbers are fiction. The unglamorous half of experiment infrastructure is making the data trustworthy — we covered the statistics in A/B testing statistics for people who ship; the engineering obligations are:
Pre-register, mechanically. Enough teams have listened to the sermon on pre-registration. The engineering version: the experiment config file (variants, split, primary metric, kill criteria, minimum runtime) is committed to the repo before the experiment starts, and the reporting job reads it. Changing the primary metric mid-flight now requires a git commit, which is exactly the right amount of friction.
Run the SRM check. Sample ratio mismatch — significantly more or fewer exposures in one variant than the split predicts — is the experiment's check-engine light. It catches broken assignment (cookie cleared, edge rule misfiring), double-counted exposures, and bot skew. Automate it as a chi-squared test in the daily report, and teach the team the rule: an SRM does not mean "the numbers are slightly off," it means "stop the experiment, the apparatus is lying."
Log assignment separately from conversion, join later. Exposure events land in the warehouse the moment the server renders; conversions trickle in for weeks. Join on the stable user key at analysis time. Folding attribution into the client pixel is how experiments quietly lose 15% of conversions to ad blockers and then declare winners on the residue.
Freeze the environment or measure the noise. Latency improvements, deploys, and marketing pushes during the experiment window are part of the apparatus. Log deploy timestamps into the same warehouse, and annotate every report with them.
The honest cost ledger
Server-side experimentation is not free, and pretending otherwise is how the industry keeps selling the snippet. Here is the real bill:
You give up the visual editor. Non-engineers can no longer drag a headline around a live preview. In our experience, this is a feature wearing a disguise — the visual editor is precisely what encourages testing cosmetic trivia at statistical power that can't detect it, which is how teams run forty experiments and learn nothing — but it is a real capability loss for genuinely useful copy tests. The mitigation: a good CMS preview plus a fast flag-driven copy file covers 90% of "marketing wants to try a headline."
Every experiment requires engineering. Each variant is real code, reviewed and tested like real code — iterate your experiment design upstream so engineers build three great candidates a quarter instead of twelve shrugs. Velocity here is a prioritisation question, not a tooling one. This is the posture we built into Larklight's test program for their marketing site: fewer, bigger, server-rendered bets.
Static-variant caching has edges. Personalised-above-the-fold pages that also run experiments need the edge split done carefully. If a page is already uncacheable and personalised, the whole question is easier — but then you weren't having a flicker problem anyway.
What you don't give up: the ability to test copy, layouts, offers, flows, and pricing structures. You give up testing them badly.
Where client-side testing is still defensible
Honesty cuts both ways. Client-side variant application is acceptable when the tested element sits well below the fold (no first-viewport shift), when the audience is logged-in employees on an internal tool (performance budgets are different), or during a two-week discovery spike where the only question is directional and the code is explicitly disposable. Even then: no anti-flicker curtain, ever. A page hidden for seconds to protect a headline test has the causality exactly backwards.
And if you're already married to a client-side tool mid-contract: at minimum, self-host the snippet, cap its payload, scope its activation to the experiment pages only, and put a review date in the calendar. The migration to server-side is the kind of platform work we fold into a growth engagement — it pays for itself in Core Web Vitals debt alone.
Key takeaways
- Client-side testing tools corrupt what they measure: the variant arrives late and shifts the layout, so shipping the "winner" rarely reproduces the win.
- Assign at the edge into a signed cookie, render the variant server-side, and log exposure from the render — nothing flickers and nothing can be blocked selectively.
- Static sites split at the CDN: two pre-rendered variants, cache keyed on a rewritten experiment header, never on the raw Cookie.
- Flags are read once, at render: never re-read client-side, never hide losers with CSS, and never let "temporary" test code outlive its expiry date.
- Metric integrity is engineering: committed pre-registration configs, automated SRM checks, warehouse-joined exposure and conversion, deploys annotated on every report.
- The true cost is losing the visual editor and requiring engineering per test — buy back the copy-test use case with a flag-driven content file and call it a fair trade.
FAQ
Doesn't server-side testing make experiments slower to launch? Yes — deliberately. A variant is code, so it gets review, QA and a real deploy. In exchange you get experiments whose results you can act on. Teams that balk at the overhead are usually running too many low-value tests; the friction is the filter, and pre-registered kill criteria become natural when every experiment cost someone a sprint day.
How do you test variants on a page that's already personalised? Personalisation already broke static caching for that view, which means you have a server in the render path — so server-side testing gets easier, not harder: the variant is one more input to the personalised render. The hard combination (personalised and CDN-cached) is resolved by splitting at the edge on the assignment header, exactly as for static variants.
What sample size do we need before any of this matters? A rough floor: if a variant change would realistically move conversion by less than ~10% relative, you need tens of thousands of visitors per arm, and most sites shouldn't be A/B testing that page at all — they should be running bigger swings or qualitative research. Compute power before building, not after the flat result.
Can we keep our existing testing tool and just self-host the snippet? You'll remove the third-party latency and some of the blocking, but not the fundamental flaw: the variant still arrives as a DOM mutation after first paint, so layout shift and the measurement confound remain. Self-hosting is a fine bridge measure; it is not the destination.
Web workers for real work: what we move off the main thread
Web workers aren't a WebGL party trick. CSV parsing, fuzzy search, image pipelines, zipping exports — how we move real work off the main thread without the plumbing pain.
Streaming SSR: practical notes from shipping it
Streaming SSR in practice: what actually streams, where Suspense boundaries help or hurt TTFB, hydration ordering, mid-stream errors, and how to measure it.
The caching layers cake: HTTP, ISR and the discipline of invalidation
A layered caching model for content-heavy sites — CDN, full-page, data and application caches — plus the purge-on-publish pattern that ends 3am incidents.