A CAPTCHA on your auth flow shouldn’t weigh more than your auth flow. reCAPTCHA ships well over 250 KB transferred per page, runs on every page that includes it, and is part of why your Lighthouse score lives in the 60s. This article walks the numbers, the Web Vitals impact, and the budget you should hold your CAPTCHA to.
The 250 KB tax on your auth flow
Here’s the comparison we ran in early 2026 across the major CAPTCHA loaders, measured as transferred bytes (gzip on the wire) on a fresh page load:
| Vendor | Bundle (gzipped) | Requests on init | Origins reached |
|---|---|---|---|
| reCAPTCHA v3 | ~250 - 290 KB | 5 - 9 | 2 (google.com, gstatic.com) |
| reCAPTCHA v2 (image) | ~210 - 250 KB | 5 - 8 | 2 (google.com, gstatic.com) |
| hCaptcha | ~75 - 90 KB | 3 - 5 | 2 (hcaptcha.com, newassets.hcaptcha.com) |
| Cloudflare Turnstile | ~45 - 55 KB | 2 - 3 | 1 (challenges.cloudflare.com) |
| Friendly Captcha | ~20 - 30 KB | 1 - 2 | 1 (friendlycaptcha.com) |
| Playtcha | ~14 KB | 1 | 1 (playtcha.com) |
Numbers measured against each vendor’s default integration on a clean Chrome profile, March 2026. Treat them as ballpark; re-measure in your environment before relying on the deltas.
The headline: you can spend 14 KB or you can spend 250 KB on the same defensive layer. The 18× difference doesn’t buy you 18× better bot detection — it buys you a behavioral scoring engine you may not need, and a Google or Cloudflare sub-processor you definitely have to document. We covered the privacy angle in privacy-first CAPTCHAs explained.
What reCAPTCHA actually loads
Open Chrome DevTools, Network tab, load a fresh tab against a page with reCAPTCHA v3 mounted. You’ll see roughly:
GET https://www.google.com/recaptcha/api.js ~7 KB
GET https://www.gstatic.com/recaptcha/releases/.../recaptcha__en.js ~220 KB
GET https://www.google.com/recaptcha/api2/anchor ~12 KB
GET https://www.google.com/recaptcha/api2/bframe ~8 KB
GET https://www.gstatic.com/recaptcha/.../webworker.js ~3 KB
... and several more iframe + asset requestsThat’s before the user clicks anything. Therecaptcha__en.js bundle is the bulk: it contains the behavioral-signal collection, the scoring shim, the challenge-rendering UI for the v2 fallback, and the i18n strings for the locale you happen to load.
For comparison, the entire Playtcha widget — game engines, three minigames, audio mode, success/failure UI — is about 14 KB gzipped because we don’t ship behavioral scoring, we don’t ship i18n in the bundle (it lazy-loads when needed), and we don’t ship a behavioral-fingerprint collector.
Core Web Vitals impact
Bundle weight is a proxy for what really matters: user-perceived performance. The relevant metrics:
Largest Contentful Paint (LCP)
If your CAPTCHA loader blocks rendering of the LCP element (a hero image, a sign-in card), you push LCP into the “needs improvement” or “poor” band. The threshold for “good” LCP is 2.5s on the slowest 75% of visits. A 250 KB blocking script on a slow connection can eat that budget by itself. Google’s own LCP guidance is the canonical reference.
Interaction to Next Paint (INP)
INP replaced FID in March 2024 as a Core Web Vital. It measures the time between a user interaction and the next paint. A CAPTCHA loader that runs heavy JavaScript on the main thread during page load can push INP past the 200ms “good” threshold by occupying the thread when the user tries to interact. The behavioral CAPTCHAs do exactly this — they install listeners that fire on every mousemove and keystroke and post signals back to the vendor.
Cumulative Layout Shift (CLS)
Less directly affected, but CAPTCHA widgets that render an iframe with a non-reserved size cause layout shift when they mount. Reserve the slot with explicit width and height in your CSS, regardless of which CAPTCHA you ship.
A sane bundle budget for your auth flow
Set a budget. Hold third-party loaders to it. Here’s the budget we’d defend in any code review:
- Total third-party JS on auth pages: < 100 KB gzipped. CAPTCHA, analytics, error reporting — combined.
- CAPTCHA share of that: < 30 KB gzipped. If your CAPTCHA is 30% of your third-party budget, the trade has to be worth it.
- No script blocks first paint. Async or defer, every time.
- No cross-origin font in the CAPTCHA bundle. Fonts are layout-blocking.
- No third-party iframe over 320×100 px on the critical path. Iframes are their own page; their JS is opaque to your bundler and your CSP.
Hold your vendors to this. If a sales conversation hand-waves bundle weight, the sales engineer probably hasn’t looked. Ask for the exact transfer size on a fresh load with no cache. Most vendors will quote you their gzipped JS file size and omit the iframe payload, the WebWorker, and the i18n shim. Ask for the total.
What <20 KB looks like
Building a CAPTCHA in <20 KB requires deliberate decisions about scope. The decisions we made at Playtcha:
- No behavioral scoring. The biggest single driver of bundle weight. We don’t collect mouse paths, so we don’t need the collector. See privacy-first CAPTCHAs.
- No fingerprint library. No canvas hash, no WebGL probe, no font enumeration. The challenge is the proof.
- Minimal i18n in the bundle. The widget loads strings for the current locale on demand; everything else is lazy-loaded only if the user explicitly switches.
- One origin. We load the widget JS, we POST to the same origin to verify. No CDN sharding, no font CDN, no analytics endpoint.
- No animation library. The minigames use raw canvas + a few hundred lines of physics code. No Three.js, no PixiJS, no Anime.js.
- Treeshaken aggressively. We ship one bundle per game and lazy-load the games on demand once the widget mounts. The initial 14 KB only includes the loader and the picker.
These trades are not free. We can’t do invisible behavioral scoring; we don’t support a custom font per-customer; we can’t ship 50 minigames without rethinking the lazy-load strategy. The trade is intentional, and the bundle is the visible result.
The mobile cost is bigger than the desktop cost
Bundle weight on a fiber connection is a rounding error. On a real mobile device — a mid-range Android on a 4G connection in a building with poor reception — the cost is meaningful in two ways. First, transfer time: 250 KB on a throttled 3G connection is roughly 2-4 seconds of download. Second, parse and execute time: parsing 250 KB of JavaScript on a low-end Android CPU can take 200-400ms of main-thread time before the script does any actual work. Both come out of your INP and LCP budget.
If your analytics show meaningful mobile traffic from emerging markets, the bundle math gets even harsher. We’re deliberate about keeping the Playtcha widget tiny precisely because the worst-case device is what defines “good UX” for a CAPTCHA that gates your auth.
How to measure on your own site
The easiest measurement is in Chrome DevTools:
- Open your auth page in an incognito window.
- DevTools » Network tab.
- Set throttling to “Fast 3G” (representative of your slowest 25% of visitors).
- Disable cache, then hard reload.
- Filter by your CAPTCHA vendor’s domain.
- Read the “Transferred” column total at the bottom.
That number is what your visitors actually pay. Compare it to your total page transfer. If your CAPTCHA is more than 20% of your page weight, you’re paying a tax that has very little to do with verification quality.
For continuous measurement, ship a Lighthouse-CI run on your auth pages and gate PRs on a budget regression. Both the Lighthouse CI tooling and the WebPageTest API can do this.
What to budget for the rest of your auth page
Your CAPTCHA isn’t the only third-party loader on the auth page. Common loaders we see eating bundle budget:
- Analytics (15-40 KB for the privacy-first ones, 80+ KB for the heavyweights).
- Error reporting (15-50 KB for Sentry / Bugsnag / equivalent).
- Customer-success widgets (Intercom, Drift, etc.) — often 200-400 KB and a major source of LCP regressions.
- Payment-form SDKs (Stripe Elements is ~50 KB; alternatives vary).
- A/B-testing scripts (often 30-100 KB and main-thread blocking).
Your auth page should not have all of these. We’ve seen production auth pages where the user is fighting against 700+ KB of third-party JS just to fill in an email and password. The CAPTCHA share of that mess is usually 20-30%. Reducing it is a measurable Web Vitals win.
Yes, you can lazy-load — but read this first
A common reaction to CAPTCHA bundle bloat is “just lazy-load it on form focus.” This works, sort of. The gotchas:
- Lazy-loading delays the verification UI by however long the bundle takes to download. On a 3G connection, that’s a noticeable lag the user perceives as “the form is broken.”
- Behavioral CAPTCHAs need the signal-collection script running for a while to score the user. Lazy-loading on submit gives them less data to work with, which raises false positives.
- If your CAPTCHA mounts inside a Suspense boundary or a dynamic import, you have to handle the loading state explicitly, which adds UI complexity you can’t hide.
Lazy-loading is a useful mitigation for a heavy CAPTCHA, but it’s a workaround, not a solution. The real solution is to pick a CAPTCHA that doesn’t need to be lazy-loaded at all because it’s small enough to load on the critical path without paying a Web Vitals tax.
Related: the broader vendor comparison, in reCAPTCHA alternatives. The privacy posture that lets us ship in 14 KB, in privacy-first CAPTCHAs explained. The bot-economics view of why a smaller CAPTCHA isn’t a less-secure CAPTCHA, in the economics of CAPTCHA bypass. The implementation walkthrough that demonstrates the loader, in CAPTCHA with Supabase. And the upstream “do I need this at all” question, in why use a CAPTCHA.
The network tax is the cheap cost; the CPU tax is the expensive one
Bundle size is the metric most teams look at because it’s the easiest to measure. The cost that actually shows up in Web Vitals, though, is the CPU time spent parsing and executing the script after it arrives. A 250 KB bundle downloads in ~1 second on a 4G connection — annoying but survivable. The same bundle takes 200-500ms to parse on a mid-range Android CPU and another 100-300ms to execute, all on the main thread, blocking interaction.
The implication for CAPTCHA selection: if a vendor’s bundle is small but does heavy work on every page (e.g. installs persistent mousemove listeners, runs a behavioral scoring loop), the network number understates the real cost. Always profile main-thread time alongside transferred bytes. Chrome DevTools’ Performance tab will show you exactly which functions in the vendor’s bundle are eating your thread.
FAQ
Is gzipped or transferred the right number?
Transferred. That’s what your user’s phone actually downloads. Gzipped is a measurement of the file on disk; transferred is the wire size, which is what affects load time and Web Vitals. Vendors quote whichever is smaller.
Does Brotli help?
Yes, modestly. Brotli usually shaves 15-25% off the transferred size compared to gzip. Most CDNs serve Brotli to modern browsers automatically. Check your network panel’s Content-Encoding header to confirm.
What about HTTP/3 and parallelism?
HTTP/3 and HTTP/2 multiplexing help with the request-overhead cost of fetching many small files. They don’t reduce total transferred bytes, and they don’t reduce the JS parse and execute time on the main thread, which is often the larger Web Vitals cost.
Is the Playtcha widget really 14 KB?
The initial loader bundle that mounts on your page is in that range. Playing a game lazy-loads the game-specific code (a few additional KB per game). The total for a single verification is in the 25-30 KB range, still well below alternatives.
Can I host the CAPTCHA loader on my own CDN?
For Playtcha specifically, no — we serve the widget from our own edge network. Self-hosting would break our token-binding model and complicate updates. That’s a deliberate trade. If you have an Enterprise requirement to self-host, talk to us.