Flicker and FOOC
Client-side experiments wait for the page to load, pick a variant, then swap the DOM. That swap happens after the original markup has already painted, so users in the treatment see the control for a beat before the variant appears. That’s flicker, or FOOC - flash of original content.
It matters because it taints the treatment in two ways. Users saw the control before the swap, so any decision they made off the first impression is a decision off the control, not the variant. And the swap itself draws attention to whatever changed, which can inflate engagement with the treatment in a way that won’t replicate once the test ships and the swap goes away. The CLS hit from the layout shift is often measurable in its own right.
Why it happens
Section titled “Why it happens”Two things have to finish before a variant can render:
- The experiment SDK has to load and execute.
- The variant decision has to be made - user bucketed, feature flag evaluated, treatment fetched.
Both happen in JavaScript, after the browser has parsed and painted the HTML. The original markup paints because nothing told the browser to wait. The fix is to tell the browser to wait. Waiting is exactly what hurts page speed, so this is a tradeoff, not a free win.
The standard anti-flicker pattern
Section titled “The standard anti-flicker pattern”Hide the page until the SDK has decided. Then reveal.
<head> <style id="antiflicker">body { opacity: 0 !important }</style> <script> setTimeout(function () { var s = document.getElementById('antiflicker'); if (s) s.remove(); }, 2500); // safety net if SDK fails </script></head>The SDK removes the #antiflicker style as soon as it finishes evaluating. The setTimeout is the safety net. If the SDK is slow, blocked by an ad blocker, or the request fails, the page reveals itself after the timeout instead of staying blank forever.
The timeout is the entire question. Too short and you reintroduce the flicker. Too long and slow connections see a blank page that looks broken. 2 to 4 seconds is the common range. I default to 2.5 unless the SDK is unusually heavy.
Scope the hide
Section titled “Scope the hide”Hiding body is the lazy version. If the variant only touches a hero block, hide only the hero:
.hero { visibility: hidden }The SDK removes that one rule. The rest of the page paints normally and only the hero waits. LCP improves if the hero isn’t the largest element. CLS still needs handling - reserve the hero’s height so removing visibility doesn’t push content down.
When to skip the anti-flicker
Section titled “When to skip the anti-flicker”- High-traffic, high-stakes pages (PDPs, paid landing pages). The validity cost of flicker is bigger than the LCP cost of hiding. Anti-flicker on.
- Long-form content, blogs, lower-funnel pages. The variants are usually small and the LCP cost outweighs the validity hit. Let it flicker, or move the test server-side.
- Anything where the variant is below the fold. Don’t hide the whole page for a change the user can’t see yet. Scope the hide, or skip it.
Where it falls down
Section titled “Where it falls down”Single-page apps. The “page” never reloads, so the anti-flicker pattern only fires on the first route. Subsequent route changes need the hide re-triggered manually, and most teams don’t bother. SPA tests end up flickering more than the marketing site sat next to them.
Personalisation has the same flicker problem and the same fix. The wrinkle is that personalisation logic often runs longer - segment lookups, profile fetches - so the timeout has to be longer, which makes the page-speed cost worse.
Server-side experimentation skips this entire problem. The HTML arrives with the variant already in it, no JS swap, no flicker. On Shopify that means Liquid-level branching, with its own constraints around caching.