Skip to content
Front-end optimisation

A/B Tests Without Flicker: Keeping LCP Under Control

Why anti-flicker layers push out Largest Contentful Paint, how to measure their share of the first paint and which setups carry a test without hiding the page.

15 min read A/B-TestsLCPFrontend

An A/B test is meant to find out which variant works better. So that visitors do not see the old version first and the new one a moment later, many setups hide the entire page until the testing tool has made its decision. That very move costs the metric the same test is later measured against. The classic pre-hiding snippet gives itself up to 3,000 milliseconds (Vendor documentation) before releasing the page on its own – and until then there is nothing to see. On mobile only 62 percent (Web Almanac 2025) of pages reach a good Largest Contentful Paint in the first place. A hiding layer pushes exactly that measurement point further back, because an invisible element is not a candidate at all as far as the browser is concerned. This article shows how the connection arises technically, which numbers sit behind it and how a test can be shipped without sacrificing the first paint. In our frontend optimization work, the test layer is one of the first items we record.

Key takeaways

  • Flicker happens because the baseline version is already painted when the test script answers. The usual countermeasure hides the page on suspicion – one vendor's pre-hiding snippet releases it again after 3,000 milliseconds (Vendor documentation).
  • Hiding shifts more than the impression, it shifts the measurement: Chromium excludes elements with an opacity of 0 (web.dev) as LCP candidates. The measurement point therefore moves to the end of the hiding period.
  • This hits the metric at its most sensitive spot, because on 76 percent (Web Almanac 2025) of mobile pages the LCP element is an image – precisely the element the layer covers.
  • Targeted blocking is the cleaner alternative: blocking="render" has been available since Chrome 105 (MDN) and Safari 18.2 (MDN), while Firefox has not implemented it. The standard defines exactly one keyword (WHATWG) for it.
  • Without measurement every decision stays a matter of taste. Only 15 percent (Web Almanac 2025) of mobile pages pass the render-blocking resources audit – so a test script rarely arrives in a tidy situation.

Why a test makes the page invisible

The sequence is much the same in almost every setup. The document arrives from the server, the browser starts painting, and only then does the testing tool report back with the decision about which variant this visitor should see. Between the first paint and the rewrite there is a visible jump: the headline changes, the button changes colour, a section disappears. Technically this is the flash of original content, in everyday language it is flicker. It is not a fault of the tool but the consequence of an order of events – the browser finishes before the decision arrives. Because that jump is immediately obvious in user testing, many setups reach for the simplest remedy: they make the page invisible until the decision is in. How strongly such interposed layers shift the first impression is shown in a comparable form by the article on consent banners and Core Web Vitals.

Technically the move could hardly be simpler. A small snippet in the document head sets a class on the root element, a rule then sets the body's opacity to zero, and the test script removes the class again as soon as it is done. So that the page does not stay blank forever in case of failure, the snippet carries a timeout. That value is the core of the problem: the vendor documentation on pre-hiding names 3,000 milliseconds (Vendor documentation) as the default deadline after which the layer removes itself. That is the upper waiting time, not the usual one – but it is the promise the setup makes to the visitor. If the tool's answer fails to arrive because the network is poor or a blocklist blocks the address, the visitor stares at a white surface for up to three seconds.

Flicker is a symptom, not a cause

Hiding the page treats the visibility of the problem, not its source. The source is the order of events: an external script decides on content that has already been painted. Anything that moves the decision earlier therefore helps more than anything that pushes the paint later. That includes limiting the test to areas that become visible later anyway, moving variant selection ahead of delivery, or building the baseline version so that a switch produces no jump. Only once that is exhausted does the question of how long the page may stay invisible become relevant.

What the hiding layer does to LCP

The connection is not merely a feeling, it is written into the browser's selection rule. For Largest Contentful Paint, Chromium looks for the largest element in the viewport – and explicitly excludes elements with an opacity of 0 (web.dev) because they are invisible to the user. So while the hiding layer is in place, there is no valid candidate. The clock keeps running regardless, because measurement starts at navigation start. As soon as the layer drops, the largest visible element is recorded – with the time elapsed until then. An LCP of one second turns into two or three seconds without any change to the content. 2.5 seconds (web.dev) counts as good, and not on average but for 75 percent (web.dev) of page visits. How the four sub-parts behind that value can be broken out individually is described in the article on the four phases of LCP.

This gets particularly awkward because the layer usually covers exactly the element that matters. On 76 percent (Web Almanac 2025) of mobile pages the LCP element is an image, meaning the hero image, the product photo or the stage. When two self-inflicted brakes meet, the damage adds up: 16 to 17 percent (Web Almanac 2025) of pages lazy-load their LCP image, and only 17.3 percent (Web Almanac 2025) of mobile pages with an image LCP set the high fetch priority. Adding a hiding layer on top delays an element that is already late. Which levers work here is sorted out by the article on priority hints and fetchpriority. The first paint suffers in the same way: a good First Contentful Paint is 1.8 seconds (web.dev) – a page that stays invisible for three seconds breaks that mark before any content has loaded at all.

measure-hiding.js
// Measures how long the hiding layer holds back rendering -- and which candidate
// the browser records as Largest Contentful Paint afterwards.
const marks = [];

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    marks.push({
      metric: 'LCP',
      time: Math.round(entry.startTime),
      element: entry.element ? entry.element.tagName : '(no element)'
    });
  }
}).observe({ type: 'largest-contentful-paint', buffered: true });

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.name === 'first-contentful-paint') {
      marks.push({ metric: 'FCP', time: Math.round(entry.startTime) });
    }
  }
}).observe({ type: 'paint', buffered: true });

// The moment the test layer releases the page again. The class name comes from
// your own pre-hiding snippet and has to be adjusted per setup.
const root = document.documentElement;
const watcher = new MutationObserver(() => {
  if (!root.classList.contains('test-hidden')) {
    marks.push({ metric: 'page released', time: Math.round(performance.now()) });
    watcher.disconnect();
  }
});
watcher.observe(root, { attributes: true, attributeFilter: ['class'] });

// Only report on unload: before that the LCP entry is not final.
addEventListener('pagehide', () => console.table(marks));

The snippet answers the one question that precedes every decision: how much time passes between loading the document and the moment the test layer releases the page again – and which element carries the LCP afterwards. What matters is less the single value than the distribution. A lab run on a fast connection rarely shows the layer at its worst; only field values make the timeout visible, because that is where the visits land in which the tool's answer did not arrive. Why field and lab data complement rather than replace each other is described in the article on field data and lab measurement side by side. And because the timeout hits rarely but expensively, it belongs in an evaluation as its own distribution, not as an average.

The situation a test script walks into

A testing tool rarely lands on a tidy page. 90 to 92 percent (Web Almanac 2025) of all pages embed at least one third party, and the median mobile page already requests 79 requests (Web Almanac 2025) from external origins. Scripts make up the largest block: 24.8 percent (Web Almanac 2025) of all third-party requests. On top of that comes nesting – the median depth of the inclusion chain is 3 (Web Almanac 2025), so an embedded tool usually pulls in further tools. How this inventory can be thinned out systematically is described in the article on trimming third-party scripts. For a test setup this means the test script is not the only guest, but often the only one that ties the display of the page to itself.

The waiting time is the promise

A timeout is not a safety net but an upper bound on blind time. What is meant as an exception becomes the rule for a share of visits – namely those on slow networks, whose values are already the worst in any evaluation.

The main thread is already busy

The median Total Blocking Time on mobile is 1,916 milliseconds (Web Almanac 2025). A synchronously loaded test script piles on top – any task over 50 milliseconds (Chrome for Developers) counts as a long task and delays the response to input.

The test measures itself

If hiding shifts the paint equally in both variants, the comparison stays clean. But the test measures the absolute effect on revenue and bounce rate against a page it slowed down itself – which means the gain from the better variant is understated.

SetupWhat the visitor sees until thenEffect on LCPBehaviour on failure
Hide the whole page, script loaded asyncnothingmeasurement point moves to the end of hidingblank page until the timeout
Test script deliberately render-blockingnothing, but noticeably shortermeasurement point tied to the script's durationbrowser paints on its own time limit
Variant decided before deliverythe finished variantunchangedbaseline version is delivered
Test limited to areas below the foldthe real header areaunchangedarea stays in the baseline version
LCP element kept out of the testthe real header areaunchangedno effect on the display

Block deliberately instead of hiding everything

The platform offers a dedicated means for exactly this case. With the attribute blocking="render" a script can be embedded so that the browser only begins rendering after it has run – without the page having to be hidden for it. The difference is not cosmetic: instead of a fixed three-second deadline, the delay is tied to the script's actual duration, and the browser keeps control over the moment of first paint. The standard states that a blocking attribute explicitly indicates which operations depend on fetching an external resource; the list of possible values contains exactly one keyword (WHATWG). The platform offers no finer control – which keeps things clear, but also means there is no gradation by area.

Availability is the catch. The compatibility data lists the attribute on the script element from Chrome 105 (MDN) and from Safari 18.2 (MDN); for Firefox it records version_added: false (MDN), on the link element for stylesheets and preload references as well. Relying on it alone leaves a share of visitors in exactly the starting position: script arrives late, page is already painted, jump visible. A two-stage setup is therefore practical – targeted blocking where it is supported, and a more narrowly scoped fallback where it is not. Historically this is nothing new: parser-blocking external scripts via document.write were harmful enough for the browser vendor to intervene; by its own measurement 7.6 percent (Chrome for Developers) of all page loads on 2G were affected.

The core in one sentence

A hiding layer turns a display problem into a measurement problem: it prevents flicker by shifting the moment at which anything becomes visible at all – and that moment is precisely the metric the page is subsequently judged by.

Where the keyword is missing: the second route

The most robust way out moves the decision ahead of delivery. If the variant is already chosen while the response is assembled – on the server or at the delivery layer in front of it – only one version reaches the browser. There is no flicker because there is nothing to replace, and no hiding layer because nothing is rewritten afterwards. The price is time elsewhere: variant selection then sits in the critical path of the server response, and there is little room there. Only 44 percent (Web Almanac 2025) of mobile pages have a good time to first byte. Where that time actually goes in the backend can be broken down with the means from the article on server response time. Second price: the response becomes specific per variant, and caching has to reflect that rather than being rendered useless by it.

Three patterns that make a test expensive

First, testing on the LCP element itself: whoever tests the hero stage cannot paint it early at the same time. Second, hiding the entire body although the test only affects a button further down – the layer then costs for the whole screen what is needed for one section. Third, the timeout as a permanent state: if the tool's answer regularly fails to arrive, the setup does not hide briefly but reliably long. None of the three show up in a lab run on a fast connection – they appear only in field values, and usually in the worse quartile.

An approach that works in an existing setup

In day-to-day operation the big rebuild is rarely the right choice, because the test layer is tied to processes that do not sit in development. A sequence that starts with the biggest lever and evidences each step individually is more useful. As a reference point, the overall picture is worth a look: only 48 percent (Web Almanac 2025) of mobile pages pass all Core Web Vitals, and the median mobile home page weighs 2.56 MB (Web Almanac 2025) with 646 KB (Web Almanac 2025) of JavaScript and 22 requests (Web Almanac 2025) for scripts. So a testing tool is added to an already well-filled cart. How to write down an upper bound for that is described in the article on performance budgets for JavaScript.

  1. Take inventory: which tests are running, on which templates, with which hiding mechanism and with which timeout? Tests whose evaluation finished long ago often keep running.
  2. Shrink the hidden area: instead of the whole body, cover only the section the test actually changes – this keeps the LCP candidate visible and the measurement undisturbed.
  3. Take the LCP element out of the test. Where the hero stage itself is to be tested, the decision belongs ahead of delivery and not in a script loaded afterwards.
  4. Where supported, embed the test script as deliberately render-blocking and set the hiding timeout well below the vendor default – the fallback is the baseline version, not the blank page.
  5. Measure before and after in the field, split by variant and device, and record the test layer as its own line item in the budget, as described in the article on budgets in the build pipeline.

A sober look at the metrics a test actually touches helps with the assessment. Layout shift is rarely the problem: 81 percent (Web Almanac 2025) of mobile pages have a good CLS value. Responsiveness, by contrast, suffers when the test script occupies the main thread – on mobile 77 percent (Web Almanac 2025) reach a good INP value, on desktop it is 97 percent (Web Almanac 2025). How long tasks can be broken up so that the browser can paint in between is shown by the article on breaking up long tasks. It shows up in the lab score too: the green range only starts at 90 to 100 (Chrome for Developers) points, and how to read such a report without working through it from top to bottom is described in the article on reading a Lighthouse report.

A test that makes the page invisible for three seconds no longer measures the effect of the variant. It measures how many visitors are willing to wait for it.

Project experience from performance work for shops and portals

The trade-off can be quantified commercially. An analysis of mobile site data from retail, travel and lead generation found that a 0.1 second faster load time lifted retail conversion rates by 8.4 percent (Deloitte, commissioned by Google). A test that costs half a second has to earn that effect back before it shows any gain at all. This is felt most where tests run most often: 19.2 percent (Web Almanac 2025) of the mobile sites examined are ecommerce. How speed requirements can be recorded bindingly instead of being negotiated after the fact is described in the article on performance in tenders; and that the effect on weak hardware differs from the one on a development machine is shown by the article on the target device instead of the test device.

Handling tests is therefore less a question of tooling than of order. Whoever makes the variant decision as early as possible, keeps the covered area as small as possible and takes the LCP element out of the script's reach can run tests without pledging load time as collateral. Where exactly the time goes and which template delivers the worst values is clarified by a technical performance analysis with field and lab data side by side – and how the results show up in the metrics that search engines and visitors alike judge is sorted out on our page about Core Web Vitals.

Sources and studies

This article draws on data from the HTTP Archive Web Almanac (2025 edition), on web.dev, on the developer documentation from Chrome for Developers, on the browser compatibility data from MDN, on the WHATWG HTML Living Standard, on a vendor documentation page about pre-hiding, and on an analysis of mobile site data by Deloitte. The figures quoted refer to the state of the respective publication.

Related Articles

Front-end optimisation

fetchpriority: Speed Up LCP with Priority Hints

How fetchpriority='high' loads the LCP image sooner: correct use, interplay with preload and srcset, the most common mistakes and the LCP quick win.

13 min read
Fundamentals & strategy

Critical CSS: Visible Area Loaded in Milliseconds

Extract, inline and automate Critical CSS: load the visible area in milliseconds, keep the rest out of the way and improve your LCP score for the long run.

13 min read
Front-end optimisation

Style Recalculation: When CSS Slows the Browser Down

Why style calculation costs time at runtime, how Selector Stats and Long Animation Frames make its share visible, and which changes actually shrink the scope.

14 min read