← All posts
technical flicker QA optimisation

Why your A/B test flickers — and how to actually fix it

Anti-flicker techniques demystified: why naive snippet placement causes visible page jank, what actually works, and why most implementations get it wrong.

4 min read

Every CRO practitioner has seen it: you visit a page under test and for a brief, horrible moment you see the original — the control — before the variant snaps into place. It lasts 100–300 milliseconds. Users notice. It destroys the validity of your test.

Here is why it happens and what to actually do about it.

Why the flash exists

A/B testing tools that operate client-side work by intercepting the page after the browser has already begun rendering the HTML. The document parses top-to-bottom. By the time your testing snippet runs, the browser may have already painted the original content.

The naive fix — and what most tools default to — is a brief “anti-flicker snippet” that hides the page until the variant has been applied:

(function() {
  var style = document.createElement('style');
  style.id = 'anti-flicker';
  style.innerHTML = 'body { opacity: 0 !important; }';
  document.head.appendChild(style);

  setTimeout(function() {
    document.getElementById('anti-flicker').remove();
  }, 3000);
})();

This works, after a fashion. But it has two failure modes that nobody mentions.

The timeout is a confession. You are setting a 3-second maximum blank screen as your safety net. If the testing tool’s CDN is slow, or the user’s connection drops, they sit on a white page for three seconds before seeing anything. For e-commerce, that is a meaningful abandonment risk — you have introduced a CRO defect in order to run CRO tests.

It hides the whole page. body { opacity: 0 } is a sledgehammer. You do not need to hide the entire document — only the specific elements your test modifies.

What works better

Target only what you modify

If your experiment changes the hero section, hide only the hero section:

(function() {
  var style = document.createElement('style');
  style.id = 'af-exp-123';
  style.innerHTML = '.hero { opacity: 0 !important; }';
  document.head.appendChild(style);
})();

Remove the style element as part of your test’s activation code, not on a timeout:

// Inside your experiment's activation function
function activate() {
  applyVariantChanges();
  var el = document.getElementById('af-exp-123');
  if (el) el.remove();
}

The timeout becomes a true emergency fallback — fire it at 1,500ms rather than 3,000ms. Now a CDN failure costs the user 1.5 seconds on one section, not 3 seconds on everything.

Place the snippet correctly

The anti-flicker style must be in <head>, before any render-critical content. If your tag manager fires it asynchronously, or after DOMContentLoaded, it is too late — the browser has already painted.

This is the single most common implementation mistake I find on audits: the snippet is technically present but fires after first paint because it is loaded through a tag manager container that itself loads asynchronously.

The correct order in <head>:

<head>
  <!-- 1. Anti-flicker style (synchronous, inline) -->
  <style id="af-exp-123">.hero { opacity: 0 }</style>

  <!-- 2. Testing tool SDK (async is fine here) -->
  <script async src="https://cdn.optimizely.com/js/YOUR_ID.js"></script>

  <!-- 3. Everything else -->
</head>

Consider whether you need client-side at all

For any experiment that modifies content above the fold — headlines, hero images, pricing — the cleanest fix is to move to flag-based, server-side delivery. The server resolves the variant before sending HTML. There is no original to flicker from.

This is not always practical, especially for agencies working across multiple client stacks. But if you are building a permanent internal testing programme, it is worth the architectural investment. I have written a separate post on this: DOM-injected vs flag-based experimentation.

The Core Web Vitals angle

Anti-flicker delays affect Cumulative Layout Shift if elements are shown after layout has stabilised. More importantly, using opacity: 0 on large sections can affect Largest Contentful Paint timing, depending on whether the browser counts a hidden-then-revealed element as “painted.”

Google has stated that intentional hidden-then-revealed patterns for A/B testing are understood and not penalised — but this is a trust-not-verify situation. Targeting only modified elements keeps the blast radius small regardless of what the guidance says today.

Summary

  • The naive full-body hide is better than nothing, but has a 3-second fallback window and conceals more than necessary.
  • Target only modified elements, and remove the anti-flicker style from your activation code, not on a timeout.
  • Verify that your snippet fires synchronously, before first paint — not via an async tag manager load.
  • If you run the same type of tests repeatedly, consider whether flag-based delivery makes more sense architecturally.

Flicker is a solved problem. It just requires solving it at the right layer.