The browser runtime should explain itself.

The ABTestly snippet is served from Cloudflare's edge network, then buckets each visitor and applies the variation in their browser. It reapplies cleanly across single page app route changes, records every accepted event, and writes those events to a canonical ledger that your final numbers come from.

The short version. Config is delivered from the edge. Bucketing is deterministic and sticky. Variations apply as JavaScript and CSS in the browser, with cleanup before re-apply on every route change. Every accepted exposure and goal lands in the exact experiment ledger, with no sampling on the numbers you make decisions from.

Edge served, applied in the browser

ABTestly serves its experiment configuration from the edge and runs everything else in the browser. The experiment config is delivered from Cloudflare's network, served across more than 300 cities, with no origin round trip. The snippet itself is about 31 KB gzipped with a loader under 1.5 KB. What actually decides a variant and changes the page happens in the visitor's browser: the runtime reads the config, buckets the visitor, and applies the variation locally. Nothing about a visitor's assignment waits on your server.

The runtime pipeline

An ABTestly experiment moves through the same six stages on every page, and each one is named in the public docs rather than hidden in a black box.

Deterministic, sticky bucketing

Bucketing is the algorithm that turns a visitor into a variant. It is deterministic, the same inputs produce the same output every time, and it uses no shared state, so every visitor's bucket is computed from their stable id alone. ABTestly follows a two step model: first decide whether the visitor enters the experiment, then decide which variant they get.

The result is that a visitor sees the same variant whether they land on the tested page or navigate to it later in the session, and the same variant every time they return. The full method is public in the bucketing docs.

Applying variations

On the initial page load, the snippet boots, evaluates targeting and audiences for each experiment, and for every eligible one computes a bucket, fires an impression beacon, injects the variant's CSS into the head, and runs the variant's JavaScript once. Registered apply callbacks then fire. This is code first: the variation is the JavaScript and CSS you authored, running in place. For how those variations are written, read code first A/B testing with JavaScript and CSS.

What a visitor sees before the variation applies

ABTestly does not hide your page while the runtime loads. That is a decision, not a gap, and it has a cost worth stating plainly. The install snippet injects the runtime as a dynamic script, which does not block the parser, so the browser paints the page your server sent and the variation lands when the runtime arrives and runs. On a test that changes content above the fold, a visitor whose browser has not already cached the runtime can see the original first.

Hiding the page is the usual answer, and the reason we do not do it by default is that hiding works by delaying paint. An anti flicker overlay covers the page until the variation applies, which pushes every visitor's largest contentful paint later by however long the runtime takes to arrive, including the visitors who were never bucketed into any experiment. ABTestly's own speed guardrail flags a variant as slower when its p75 largest contentful paint sits 400 ms or more above control. A default that hid every page on every load would ship the regression the guardrail exists to catch.

So here is how long the gap actually is. Measured on 20 September 2026 over 200 page loads in each condition, headless Chromium against the real built runtime, served gzipped under the same cache headers production sends, with one variation that rewrites a heading above the fold. Each figure is the 75th percentile of the time from navigation start to the moment that heading changed in the DOM.

Read the first visit figure as a floor rather than as field data. The measurement runs against a local origin, so it carries the throttle and the transfer but none of the DNS, TLS or edge latency a real first visit also pays, and it throttles the network without throttling the processor. A first visit on a mid range phone is slower than 529 ms, not faster.

Writing a variation that does not need the page hidden

When the runtime arrives is a property of the visitor's connection and cache. Whether they notice is mostly a property of what you wrote.

If you do need the page held until the variation lands, the install card in your dashboard has an Add anti flicker checkbox, unchecked by default. Ticking it adds a style rule that sets the page to zero opacity and a timer that reveals it after 2 seconds whether or not the runtime ever arrived. In the same measurement, ticking it held the page for a 75th percentile of 21 ms on a repeat view and 532 ms on the throttled first visit, and it does that on every load for every visitor, including the ones in no experiment. It is worth turning on for a specific test that needs it, and worth turning off again afterwards.

Single page apps: cleanup, then reapply

If your site swaps the page in JavaScript instead of doing a full browser navigation, ABTestly supports it natively. With SPA support on, the snippet wraps history.pushState and history.replaceState, listens for back and forward navigation and hash changes, and runs a lifecycle on every detected route change. The order is the whole idea, cleanup, then impression, then apply:

The three helpers make this reliable. onApply runs each time an experiment becomes active on the current route, onCleanup runs when the visitor leaves a tested route and just before it reapplies, and waitFor handles elements that render asynchronously by polling every 50 ms and firing once when the target appears, giving up silently after a timeout that defaults to 5000 ms. Every callback and the route handler itself run inside their own try and catch, so a mistake in one experiment cannot break the page or another experiment. The full reference is the single page apps docs.

Event capture

The runtime records two kinds of event, exposures and goal fires, each carrying its full context: the variant key, and for revenue goals the order id and currency. Goals can fire declaratively from the dashboard or from your own code with abtestly.trackGoal(key, valueOrOpts), where an order id enables server side revenue deduplication so a retried beacon does not double count. On a route change, impressions batch into a single beacon per route, and page visit goals evaluate again against the new URL while listener goals stay live from initial load. Sensitive fields are redacted, and a variant error is reported back on first occurrence per session so a variant that breaks on a real page gets caught early.

The exact experiment ledger

Every event that arrives is written to two places. Cloudflare Analytics Engine is fast and cheap to query and drives the live counters on the dashboard. The exact experiment ledger is the canonical store your final numbers come from. Analytics Engine is optimized for a huge write rate and can sample its own data under very heavy bursts, which is Cloudflare's documented behaviour rather than a worry of ours: at very high volumes it downsamples to maintain performance, and marks the rate on each row, which is fine for a live counter and less fine for a report. The ledger is optimized for correctness: every accepted event, in order, retrievable.

The ledger keeps two things on different clocks. The per visitor records your results are computed from persist for the life of the experiment, so a number you read today is the same number you read a year ago. The raw individual events behind them are archived for 24 months and then purged nightly, which is what raw event export and audits read from. Both are stated in the privacy policy. For any experiment you have run, the ledger contains every exposure and every goal that was accepted, and the statistics are computed against that set, with no sampling step on any surface you make decisions from. See the exact ledger docs.

Debug it on the page

When something looks off, open devtools and call window.__abtestly.debug() right on the page. It tells you whether this visitor was even in the test, whether the change actually ran, and whether the result got tracked. The runtime object is a stable public surface you can call from variant JavaScript or your own scripts, documented in the window.abtestly API reference.

Quick answers

Does the experiment run at the edge?

The config is served from Cloudflare's edge network, but the bucketing and the variation apply both happen in the visitor's browser. The edge delivers the config with no origin round trip. The browser decides the variant and changes the page.

Will a visitor always see the same variant?

Yes. Bucketing is deterministic and sticky. It is computed from a per experiment salt and a stable visitor id, so the same visitor gets the same variant whether they land on the tested page or navigate to it later, and every time they return, unless they clear their cookies and localStorage.

Does the page flicker before a variation applies?

It can, and ABTestly does not hide the page by default to prevent it. The runtime is injected as a dynamic script that does not block the parser, so on a first visit the browser paints the original and the variation lands when the runtime arrives. Measured on 20 September 2026 over 200 page loads per condition, the 75th percentile from navigation start to the variation applying was 19.9 ms with the runtime already cached, 529 ms on a first visit over a connection throttled to 1.6 Mbps down and 150 ms round trip, and 2.2 ms on a single page app route change. Hiding the page is a checkbox on your install snippet, unchecked by default, because hiding works by delaying paint for every visitor including the ones in no experiment.

How does it stay correct on a single page app?

On every route change the runtime cleans up, then records impressions before any variant code, then reapplies. Recording impressions first means a broken variant on one arm cannot undercount that arm, which protects against a sample ratio mismatch.

Are the results sampled?

No. Live dashboard counters read Analytics Engine, which can sample under heavy bursts, but every accepted event is also written to the exact experiment ledger, and the verdict is computed against that complete set with no sampling.

Behavior on this page reflects current ABTestly developer documentation. The public docs name the methods, the event path, the statistics, and the operational limits.

see it on your page

Read how it works
before you install it.

Drop the snippet on one page, open devtools, and watch the runtime bucket, apply, and track in real time.