Skip to main content

How to Fix A/B Test Flicker (The Complete Anti-Flicker Guide)

· 5 min read

If you've ever run an A/B test and noticed visitors briefly see the original page before your variant loads, you've experienced FOOC — Flash of Original Content.

It's the most common technical problem in client-side A/B testing, and most tools either ignore it or implement a solution that creates a different problem (CLS, layout shift).

This is the complete guide to fixing it correctly.

Why Flicker Happens​

Client-side A/B testing works like this:

  1. Browser parses HTML and starts rendering
  2. DOM becomes available
  3. JavaScript SDK loads (async, so this happens after initial render)
  4. SDK makes an API call to fetch experiment config
  5. API response arrives
  6. SDK applies variant changes to the DOM

Steps 1–3 take 0–100ms. Steps 4–6 take 100–500ms. During that window, the visitor sees the original (control) content. When the variant loads, the page visibly changes — a flash.

The Wrong Fix: body { display: none }​

Most A/B testing tools (including VWO's older implementation and some versions of Google Optimize) solved flicker with:

<style>body { display: none }</style>

This hides the body until the SDK removes the style. Problem solved, right? No — three problems:

Problem 1: Cumulative Layout Shift​

When display: none is removed, the browser recalculates layout from scratch. Depending on your page, this can cause elements to jump around — exactly the behavior that Google's CLS (Cumulative Layout Shift) Core Web Vitals metric penalizes.

Problem 2: Scroll position reset​

On some browsers, switching from display: none to display: block resets scroll position to the top of the page.

Problem 3: FOUC in some scenarios​

display: none can cause its own flash — from hidden state to fully rendered — that looks worse than the original flicker.

The Correct Fix: visibility: hidden​

<style id="_gs_af">html { visibility: hidden; pointer-events: none }</style>

visibility: hidden preserves the layout. Elements still occupy their space in the document flow — they're just invisible. When the style is removed, there's no layout recalculation and no CLS.

The pointer-events: none prevents any accidental clicks during the hidden period.

The Full Anti-Flicker Snippet​

<!-- Line 1: Hide the page immediately -->
<style id="_gs_af">html{visibility:hidden;pointer-events:none}</style>

<!-- Line 2: Safety for no-JavaScript browsers -->
<noscript><style>#_gs_af{visibility:visible!important}</style></noscript>

<!-- Line 3: Safety timeout — show page in 3s even if SDK fails -->
<script>setTimeout(function(){var e=document.getElementById('_gs_af');if(e)e.remove()},3000)</script>

Then your SDK script:

<script>
(function(w,d,k){
w._sf = { siteId: k, ingestUrl: 'https://api.datsnip.com' };
var s = d.createElement('script'); s.async = 1;
s.src = 'https://api.datsnip.com/sdk/tracker.js';
d.head.appendChild(s);
})(window, document, 'YOUR_SITE_ID');
</script>

The SDK itself removes the _gs_af style element after applying all variant changes — so the page becomes visible exactly when the correct variant is in place.

The Three Safety Mechanisms​

The snippet has three layers of protection:

MechanismProtects against
visibility:hidden on <html>Flicker from variant applying
<noscript> overridePage hidden forever if JS is disabled
3-second setTimeoutPage hidden forever if SDK fails to load

The 3-second timeout is a deliberate balance: long enough that 99.9% of SDK loads complete before it fires, short enough that a failed SDK load doesn't permanently break the page.

Placement in <head>​

The snippet must be the first thing in <head> — before any other scripts, stylesheets, or tags that could trigger rendering:

<!DOCTYPE html>
<html>
<head>
<!-- ✅ Anti-flicker FIRST -->
<style id="_gs_af">html{visibility:hidden;pointer-events:none}</style>
<noscript><style>#_gs_af{visibility:visible!important}</style></noscript>
<script>setTimeout(function(){var e=document.getElementById('_gs_af');if(e)e.remove()},3000)</script>
<script>
(function(w,d,k){
w._sf = { siteId: k, ingestUrl: 'https://api.datsnip.com' };
var s = d.createElement('script'); s.async = 1;
s.src = 'https://api.datsnip.com/sdk/tracker.js';
d.head.appendChild(s);
})(window, document, 'YOUR_SITE_ID');
</script>

<!-- Everything else after -->
<meta charset="UTF-8">
<title>...</title>
</head>

If you put other scripts first, the browser may start rendering before the anti-flicker style is applied.

Does Anti-Flicker Hurt Performance?​

The anti-flicker style is ~60 bytes inline — it adds nothing meaningful to page weight.

The actual performance cost is the time the page stays hidden: from the visibility:hidden style being applied to the SDK removing it. Typically 50–150ms on fast connections, up to 500ms on slow 3G.

This is a real trade-off. GhostSnip minimizes it by:

  1. Serving the SDK from a CDN with global edge nodes
  2. Caching experiment configs aggressively (30-second backend TTL)
  3. The SDK removing the anti-flicker style as early as possible — before DOMContentLoaded, immediately after variant changes are applied

Does It Affect SEO?​

No. Googlebot and other crawlers:

  1. Parse your HTML directly — they see the source, not the rendered state
  2. Don't execute JavaScript in the same pass as initial indexing
  3. Have separate rendering crawlers that execute JavaScript later

The visibility:hidden CSS is applied and removed by JavaScript. Crawlers reading your HTML won't see it at all. Your baseline content is indexed normally.

Anti-Flicker in GhostSnip​

GhostSnip's dashboard generates the complete anti-flicker snippet with your Site ID pre-filled. Go to Settings → Snippet and copy the "With Anti-Flicker" version.

Full anti-flicker documentation →