Performance
A consent banner exists to stop scripts from hurting users. It shouldn’t be the thing hurting them. ConsentLoop is engineered — and continuously measured — around the metrics that matter most: FCP, LCP, CLS, TBT, bundle strategy and network impact.
Measured results
From pnpm bench in this repo (Chromium, 4× CPU throttle, 7 runs, medians; local static server — a noise floor of roughly ±15 ms applies). Raw data: bench-results.json.
| Scenario | ΔFCP | ΔLCP | CLS | TBT | Consent JS wire² | Requests | Banner visible |
|---|---|---|---|---|---|---|---|
| Baseline (no consent tool) | — | — | 0.000 | 0 ms | 0 | 0 | — |
| Returning visitor (loader fast path) | +12 ms¹ | +12 ms¹ | 0.000 | 0 ms | 2.8 KB raw / 1.49 KB gz | 1 | no banner needed |
| First visit (loader + lazy widget) | +4 ms¹ | +4 ms¹ | 0.000 | 31 ms³ | 37.6 KB raw / ~13 KB gz | 2 | 136 ms |
Single-file widget (data-auto, worst case) | +8 ms¹ | +8 ms¹ | 0.000 | 31 ms³ | 36.6 KB raw / 12.1 KB gz | 1 | 118 ms |
¹ Within the ±15 ms run-to-run noise of the measurement VM — some scenarios measured faster than baseline on other runs. Deferred scripts cannot block first paint. ² Local server sends uncompressed; real CDNs serve the gzip/brotli sizes quoted. ³ At 4× CPU throttle ≈ 8 ms real; this is the one-time evaluation of the full widget, scheduled after load + idle.
Why the numbers stay this way
- The smart loader is the product. The only render-path JS is a 1.49 KB script that reads the consent cookie synchronously, activates gated scripts, pushes Consent Mode defaults — and for the majority of page views (returning visitors) never downloads the UI at all.
window.ConsentLoopis a proxy that fetches the widget on demand. - Banner after first paint. The full widget waits for
window.load+requestIdleCallback; rendering is oneinnerHTMLpass into an isolated Shadow DOM with a constructable stylesheet. No frameworks, no hydration. - CLS 0.000 by construction. Every surface is
position: fixed; the host element isdisplay: contents. There is no geometry the page could re-flow around. Animations are opacity/transform only. - Zero network by default. No fonts, no images (inline SVG), no config API, no telemetry. The optional managed adapter uses a non-blocking fetch with a hard timeout and
sendBeaconreceipts. - Bundler-native path is even leaner. The ESM build is code-split: importing
consentloopcosts ~6.6 KB pre-minification (≈3 KB min+gz) on the critical path; the UI chunk loads only if a banner must render.sideEffects: false, tree-shakeable.
Performance pillars, scored
| Pillar | Weight | ConsentLoop |
|---|---|---|
| Performance (FCP/LCP/CLS/TTI/TBT) | 40% | Deferred + idle-scheduled; CLS 0; TBT ≈ 0 (0 on repeat views) |
| Bundle strategy | 25% | ~1.5 KB loader; lazy UI; ESM code-splitting; zero deps; self-hostable |
| Network impact | 20% | 0 third-party requests; 1 first-party file for returning visitors; no beacons unless configured |
| Transparency | 10% | Source-available (FSL), open methodology, reproducible bench in-repo (pnpm bench) |
| User experience | 5% | Equal-weight buttons, full keyboard/screen-reader support, reduced motion, no dark patterns |
Reproduce it
git clone https://github.com/kocsmy/ConsentLoop && cd ConsentLoop pnpm install && pnpm build pnpm bench # Chromium, 4x CPU throttle, medians of 7 runs pnpm size # enforce gzip budgets (fails CI when exceeded)
Size budgets are enforced, not aspirational: loader ≤ 1.6 KB gz, full widget ≤ 12.5 KB gz, ESM entry ≤ 7 KB gz. A PR that bloats the bundle fails.