How we test

Every score comes from real test runs on identical pages, not opinions. Here is exactly how each number is produced, which installations appear and why, and what these measurements cannot tell you, so you can check the lot yourself.

Method v2anchors fixed 23 Sep 2026

How a run works

Every installation is a small Next.js app on one host with the same page, differing only in the consent layer. The harness drives Chromium through Playwright and interleaves installations within each iteration, so a change in the measurement machine moves the whole cohort rather than one row. The current run is 2026-09-23T13-16-00-056Z;each profile has 20 loads per installation.

Every load is cold: it starts in a fresh browser context with an empty cache and no stored consent. After load the harness waits for a 2,000 ms quiet window, allows up to 15,000 ms for a banner and caps a load at 60,000 ms.

fast-desktop applies no throttling. throttled-mobile applies 4× CPU slowdown, 1,638 kbps down, 750 kbps up and 150 ms added latency. The profile name describes throttling, not phone-screen emulation. Dates are UTC.

The leaderboard opens on fast-desktop; throttled-mobile is one selection away, and there is no cache selection because every load is cold. An installation's detail page opens on whichever profile the leaderboard was showing, and links to the other.

The test installations carry their own framework setup, so a difference between two pages includes that setup. It is not a controlled estimate of SDK cost alone, which is why every cost is expressed against the no-SDK control on the same condition.

The score formula and weights

Four categories and eight measurements, every one of them a cost the page or the visitor pays. Each measurement maps to 0–100 by a linear anchor; each category is the share-weighted mean of its measurements; the categories combine by fixed weights. Anchors, shares and weights were fixed on 23 Sep 2026, before the run they score, and changing one is a method change.

Score categories, their measurements, shares and anchors
Category · measurementWeightShare0 atMeasured as
Banner Speed30 %How long a visitor waits to be asked.
Time to banner100 %5 sFrom navigation start to the banner being painted and visible.
Page Impact25 %What the consent layer costs the host page's own rendering and responsiveness, beyond the identical page with no SDK.
First-paint delay50 %3 sHow much later the page's first contentful paint arrives than on the no-SDK control.
Main-thread blocking50 %600 msTotal blocking time added beyond the control: long-task time over 50 ms, during which input cannot be handled.
Network Cost25 %What the consent layer adds to the page's network load, read off the wire rather than from what the page is allowed to see.
Bytes added60 %250 KBWire bytes beyond the no-SDK control, headers included, as the DevTools protocol recorded them.
Requests added40 %20Completed network requests beyond the control. Each is at least a round trip and a slot in the connection queue.
Visitor Experience20 %How the banner treats the person it is asking: how much of the page it hides, and whether it answers when tapped.
Viewport coverage60 %50 %Banner area as a share of the first viewport, measured when the banner is detected.
Inert after appearing40 %1 sTime between the banner becoming visible and a control passing a hit test at its own centre. Each millisecond here is not also counted in Banner Speed.

points = 100 × (1 − min(cost, anchor) / anchor) for each measurement; category = Σ (share × points) / Σ share over its measured measurements; score = Σ (weight × category) / Σ weight over the measured categories. Every measurement the host page also affects — first paint, blocking, bytes, requests — is the increase over the identical page with no consent SDK. Time to banner, coverage and the wait until the banner can be clicked are absolute, because the control has no banner. The score is rounded once, to an integer, and the band is read from the rounded number.

Browser timings on the leaderboard. FCP, LCP, TBT and CLS are shown the same way the score reads its inputs: max(0, value − control) on the profile and percentile being shown. The test site's own load time (DNS, TLS, the server's response, HTML, CSS and fonts) is the control's too, so it is taken out and what remains is the consent layer's. An installation that came out faster than the control shows 0. Detail pages keep the absolute timings, with the control's own figure beside each delay.

Bands: Good 80–100, Fair 6079, Poor below 60. Inclusive at the lower end. The middle band is most of the field.

No banner, no score. On a condition where no banner was detected there is nothing to score: the row keeps its measurements and a dash, never a number built from the measurements that remain, and coverage is never read as 0 % when there was no banner to cover anything. When the banner was detected in fewer loads than the run made, the row says so.

Provisional scores. When a measurement could not be taken, its arc stays empty, its card shows a dash and the score is computed over the remaining weight. The leaderboard row inherits the marker. Unknown, not zero. Every installation in the current run was measured on everything, so no score is provisional.

Spread. The ± beside a score is half the range the score takes when every measurement is moved to the bounds of its 95% bootstrap interval. Two scores whose ranges overlap have not been shown to differ.

Not in the score: largest contentful paint, the page's own lcp, cumulative layout shift, time to interactive, third-party hosts contacted, third-party bytes, bytes the page could see, layout shift after banner, the delivery model, the test app's framework and the SDK's licence. They are reported, not scored. LCP in particular is reported on every row and card so that a fast banner cannot hide a slower page.

Percentiles and uncertainty

The default is p75: the estimated 75th percentile of loads in a condition. p50 is the median; p95 the slower tail. Each estimate carries a 95% bootstrap interval from 1,000 resamples with a fixed seed. Overlapping intervals mean the displayed intervals do not establish a difference; they do not prove equivalence, and non-overlap says nothing about a vendor outside this run. Every detail page plots all 20 loads with the median filled, so a tie is visible as a tie.

Numbers and units

  • Time under 1 s is an integer in ms; 1 s and over has one decimal in s.
  • Bytes under 1024 are B; 1024 and over have one decimal in KB or MB. KB means 1024 bytes throughout.
  • Coverage has one decimal in %, and keeps its zero. Layout shift has three decimals.
  • Scores are integers, 0–100. Spread is ± with one decimal.
  • Precision never exceeds the measurement: if the spread is ±1.4 the score does not gain a decimal.
  • Dates are day, abbreviated month, year, in UTC.

Every metric the collector records

Definitions follow the collector, including where its algorithm differs from standard lab or field metrics.

Scored inputs

To bannerscored · Banner Speed

Time to banner

Elapsed time from navigation until a configured banner selector is detected as visible.

Why it matters. How long a visitor waits to be asked. It is the whole of Banner Speed, 30% of the score.

Reading the value. Lower is earlier. Scores 100 at 0 ms and 0 at 5 s or later.

How it is captured. A requestAnimationFrame probe checks visibility and non-zero bounds, then records performance.now(). Detection is not a separate pixel-paint measurement.

FCPscored · Page Impact

First Contentful Paint

First Contentful Paint: when the browser first paints text, an image or other content.

Why it matters. A consent script that loads synchronously holds up the page's first paint. It is the most independent cost in the score: it barely tracks any other measurement.

Reading the value. Scored as first-paint delay, the increase over the no-SDK control: 100 at no delay, 0 at 3 s — the Core Web Vitals poor line — or more.

How it is captured. The first first-contentful-paint entry from a buffered paint PerformanceObserver.

TBTscored · Page Impact

Total Blocking Time

Total observed long-task duration beyond the first 50 ms of each task.

Why it matters. Time the page cannot respond to a tap or a click because a script is busy.

Reading the value. Scored in Page Impact as the increase over the no-SDK control: 100 at none added, 0 at 600 ms — the Core Web Vitals poor line — or more.

How it is captured. A longtask observer sums max(0, duration − 50 ms) throughout collection, not just the Lighthouse FCP-to-TTI window. This is not INP.

Bytesscored · Network Cost

Bytes over the wire

Every byte that crossed the network for this page load, headers included, compressed as sent, recorded from outside the page over the DevTools protocol.

Why it matters. It is the figure Network Cost scores. The page's own view of the same load reports zero for cross-origin responses whose server withholds Timing-Allow-Origin, which is every vendor-script installation here — so on that view the vendors that disclose least would look lightest.

Reading the value. Scored as the increase over the no-SDK control: 100 at nothing added, 0 at 250 KB or more.

How it is captured. encodedDataLength from Network.loadingFinished, summed over completed http(s) requests. Redirects count as their own entries; failed and aborted requests are excluded rather than counted as zero; data: and blob: URLs never crossed a wire and are skipped. The navigation document is included, which Resource Timing excludes — both sides of the control comparison are counted the same way.

Requestsscored · Network Cost

Requests over the wire

Completed network requests the page made, as the protocol counted them.

Why it matters. Each request is at least a round trip and a slot in the browser's connection queue, whatever its size.

Reading the value. Scored as the increase over the no-SDK control: 100 at none added, 0 at 20 or more.

How it is captured. Count of the requests summed for wireBytes.

Coveragescored · Visitor Experience

Viewport coverage

Banner bounding-box area divided by the viewport area at detection.

Why it matters. How much of the page the question hides while it waits for an answer.

Reading the value. Scored in Visitor Experience: 100 at 0 %, 0 at half the screen or more. A smaller banner is not automatically a better consent interface; this measures only what it covers.

How it is captured. getBoundingClientRect() width × height divided by window.innerWidth × window.innerHeight. The bounding box is not clipped to the visible viewport.

To usablescored · Visitor Experience

Time to a usable banner

Elapsed time from navigation until the banner first has a control a click would reach: an enabled button or link whose own centre passes a hit test, with nothing in its ancestry still animating.

Why it matters. A banner can be on screen before it can be acted on. A control that is covered, set to pointer-events: none, or still sliding into place will not respond to the tap it invites.

Reading the value. Equal to time-to-banner means the banner was usable the moment it appeared. The gap between the two is scored in Visitor Experience: 100 at no gap, 0 at a second or more. It is never also counted in Banner Speed.

How it is captured. Per frame after detection: document.elementFromPoint at each candidate control's centre, descending through shadow roots, accepting the control or a descendant of it but not an ancestor. Accepted immediately when no ancestor reports a running animation, otherwise when the control's geometry is unchanged from the previous frame. Null if nothing becomes clickable within 5s of detection — unmeasured, not zero.

Reported, not scored

LCPreported, not scored

Largest Contentful Paint

Largest Contentful Paint: the latest observed timestamp for the largest eligible content element.

Why it matters. Reported, not scored. When the banner is the largest thing on screen it is the LCP element, so a fast banner makes LCP later and a slow one leaves it early — scoring it would reward arriving late.

Reading the value. The published Core Web Vitals good threshold is at most 2,500 ms. These are lab page loads, not field assessment.

How it is captured. A buffered largest-contentful-paint PerformanceObserver retains the largest startTime until collection ends.

Page LCPreported, not scored

The page's own LCP

Largest Contentful Paint with every entry belonging to the banner excluded.

Why it matters. The page's own largest paint — the cost the host pays — separated from the banner's. On this test page it lands on first paint, which is why first-paint delay is what the score uses.

Reading the value. Reported, not scored.

How it is captured. The same largest-contentful-paint observer as LCP, skipping an entry whose element sits inside the banner, climbing out of shadow roots. Decided when the entry is observed, while its element is still attached.

CLSreported, not scored

Cumulative Layout Shift

The collector’s accumulated layout-shift values during the measurement window.

Why it matters. Reported, not scored. Every banner here is a fixed overlay, so nothing below it moves: 0.000 for all but one installation. Scored, it would hand every row a free hundred.

Reading the value. The standard CLS good boundary is at most 0.1, but this collector sums the whole window rather than using the standard largest session window.

How it is captured. A layout-shift PerformanceObserver sums entries without recent user input. This lab accumulator must not be presented as a field CLS assessment.

TTIreported, not scored

Time to Interactive

A lab approximation of the time after the last observed long task.

Why it matters. Reported, not scored: it moves almost exactly with blocking time, which is scored, so scoring both would charge the same long tasks twice.

Reading the value. Lower is earlier, but this approximation has no validated good threshold.

How it is captured. The maximum of FCP, DOMContentLoaded end and last-long-task end. This is not the retired Lighthouse TTI algorithm.

Hostsreported, not scored

Third-party hosts contacted

Distinct hosts other than the page's own that the load contacted.

Why it matters. Reported, not scored. It was scored in a draft of this method and withdrawn: the time, bytes and requests those hosts cost are already scored, and what was left scored every self-hosted install a perfect hundred for being self-hosted.

Reading the value. No threshold. Zero means the consent layer was served from the page's own origin.

How it is captured. Distinct URL hosts among completed wire requests not on the page's origin.

Third-partyreported, not scored

Third-party bytes

Wire bytes that came from an origin other than the page's own.

Why it matters. On this page the only third party is the consent vendor, so this is a second, independent derivation of the bytes Network Cost already charges — which is why it is reported rather than scored twice.

Reading the value. Zero means the consent layer was served from the page's own origin.

How it is captured. wireBytes restricted to requests whose origin is not the page's.

Page-reportedreported, not scored

Bytes the page could see

Sum of resource sizes exposed to the collector, excluding the navigation document.

Why it matters. Reported beside the wire figure, which is what the score uses. The difference between the two is exactly what a vendor's host withheld from the page.

Reading the value. Not scored. Cross-origin restrictions mean this is not complete network traffic, which is why it was replaced.

How it is captured. Resource Timing entries contribute transferSize, falling back to encodedBodySize, then zero. Cache hits may therefore still contribute body bytes; unexposed cross-origin sizes can remain zero.

Banner shiftreported, not scored

Layout shift after banner

Layout-shift total accumulated after the banner is first detected.

Why it matters. Shows movement in the part of the measurement window after the banner arrives.

Reading the value. Zero means no recorded shift in that window; this metric has no separate standard threshold.

How it is captured. The collector subtracts the CLS total at banner detection from the final total. Other late content can contribute: this is a timing attribution, not proof of causation.

What gets included or excluded

An installable consent product is published when its banner renders on the test domain and the vendor’s licensing permits that domain. A load in which no banner was detected is counted and shown on the row, never dropped quietly. Internal experimental variants are not published; the no-SDK baseline is shown separately as the control every cost is measured against.

An absent provider is a fact about this benchmark, not about the provider. A run that did not complete keeps its row with a dash where the run produced nothing. Never a zero, because a zero is a measurement.

The three delivery types

Reported on each detail page under Delivery. Compare installations, not architectures in isolation.

01Self-hosted npm

The package is bundled with the test app. These installations render the banner without fetching consent state from a vendor backend. Their JavaScript and styles still contribute to the page’s work.

02Hosted backend

The package is bundled with the test app, but its provider resolves consent state through a hosted service before deciding whether to display the banner. Time to banner includes that network round trip.

03Vendor CDN script

The test app loads the vendor’s script from a remote host. Script loading, account configuration and subsequent requests can affect when the banner appears. Its bytes are counted off the wire, so a host that hides its sizes from the page is measured all the same.

How to reproduce or dispute a result

The harness, every test app and every run manifest are in the repository. pnpm bench runs the harness against the deployed targets in targets.json and writes a new directory under results/; the site is built from those directories and nothing else.

To dispute a number, open an issue with the installation, run ID and condition, or contact CookieYes, which publishes the site. A configuration fix is re-run and the new run is published beside the old one; the old one is not edited.

Changelog

The method is versioned. Every result names the method version and run it came from, and a method change never rewrites an older run.

Method changelog
v2 · 23 Sep 2026
Four categories over eight measurements: Banner Speed 30 % (time to banner, 0 at 5 s), Page Impact 25 % (first-paint delay, 0 at 3 s; blocking added, 0 at 600 ms), Network Cost 25 % (bytes added, 0 at 250 KB; requests added, 0 at 20) and Visitor Experience 20 % (viewport coverage, 0 at 50 %; wait until clickable, 0 at 1 s). Bytes and requests are read off the wire rather than from Resource Timing, so a host that hides its sizes no longer leaves a row provisional. Layout shift is reported, not scored. Every load is cold. Bands unchanged at 80 and 60.
v1 · 15 Sep 2026
The first published method. Four inputs with linear anchors — time to banner 2.5 s, transferred bytes 250 KB, viewport coverage 50 %, and page stability as the mean of CLS (0.25) and TBT (600 ms) — weights 30/25/25/20, bands at 80 and 60. Bytes came from Resource Timing; a host that hid its sizes left the row provisional.
When the method changes, historical scores are not recomputed, so a difference between two runs never measures our method rather than the provider.

What this cannot tell you

  • No consent is accepted or rejected in a run, so nothing about interaction, INP included, is measured.
  • Account settings, geography, cache state and a different app integration can change the result. These figures describe the recorded run.
  • Banner detection depends on configured selectors and a visibility check. A detected element is not a compliance audit.
  • Bytes and requests come from the browser’s network log, which is not a view of the vendor’s servers. A dash means unmeasured; it never means nothing happened.
  • The load waterfall on a detail page marks four measured moments: first paint, the largest paint, the control's largest paint, and when the banner became visible. The bars between them are drawn to fixed proportions, because a run records when each phase ended and not when the browser handed one phase to the next. Read them as shape, not as timings.
  • The site's own Lighthouse and accessibility audits are separate from the benchmark measurements in its tables.