Skip to content

perf(procaptcha): start detector assignment at page load - #2971

Merged
HughParry merged 2 commits into
mainfrom
perf/parallel-detector-assign
Aug 4, 2026
Merged

perf(procaptcha): start detector assignment at page load#2971
HughParry merged 2 commits into
mainfrom
perf/parallel-detector-assign

Conversation

@HughParry

@HughParry HughParry commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Measured with headless Chromium against the demo pages:

page load → frictionless complete
production (3.6.76, no assign step) 1744 ms
staging (3.7.0) 4928 ms

Staging waterfall:

 466 ms  procaptcha.bundle.js
 632 ms  ┐ 7 sequential dynamic-import levels (~700 ms)
1166 ms  ┘ ProviderApi chunk finally loaded
1513 ms  detector/assign starts          <-- nothing before this needed it
2448 ms  …935 ms to download the bundle
3817 ms  frictionless starts
4928 ms  done

Change

Nothing in the assign request depends on React, i18n or the widget config. It needs the site key (a DOM attribute), the environment (a build-time constant) and the IP-mode flags (DOM attributes) — all available at ~0 ms. The entry now starts it as soon as it has read them, and customDetectBot claims the in-flight promise instead of issuing its own.

The ProviderApi + provider-selector chunks now download in parallel with React/i18n rather than after them, so assign should start around 400–500 ms instead of 1513 ms.

Safety

  • Dynamic import, so the provider selector and API client stay out of the entry chunk. Entry grows 17,985 → 18,387 bytes (+402).
  • Fire-and-forget. A failed prefetch is indistinguishable from no prefetch — the existing path still resolves a provider itself, so this cannot turn a working session into a broken one. A no-op .catch is attached so it never surfaces as an unhandled rejection in the host page, while the real consumer still sees the rejection and falls back.
  • Single-use cache, keyed on (environment, ipMode, siteKey). A retry is retrying because the pinned pronode failed, so it must not reuse that pin; and a second widget with different IP flags must not claim another widget's assignment. Both are covered by tests.

Verification

  • procaptcha-frictionless + procaptcha-bundle typecheck clean
  • 24 tests pass, including 6 new ones for the cache and the pre-existing customDetectBot suite (which this PR modifies)
  • built bundle confirmed to contain startDetectorPrefetch in the entry chunk — if it landed in a lazy chunk the change would silently do nothing

IpMode is derived via Parameters<typeof getProcaptchaRandomActiveProvider>[1] rather than importing @prosopo/load-balancer, avoiding a new dependency plus tsconfig project reference for one type alias.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XJ7KiTDKiu3mxQ4iuLFK2y

The detector now comes from the provider-served pool, so the frictionless
flow cannot start until /detector/assign returns. That request was issued
by customDetectBot, which only runs once React has mounted the widget, so
it queued behind the bundle's dynamic-import chain.

Measured on the staging demo page:

  466 ms  procaptcha.bundle.js
  632 ms  7 sequential dynamic-import levels (~700 ms)
 1166 ms  ProviderApi chunk finally loaded
 1513 ms  detector/assign starts
 2448 ms  ...935 ms to download the bundle
 3817 ms  frictionless starts
 4928 ms  done   (production, which has no assign step, is 1744 ms)

Nothing in the assign request depends on React, i18n or the widget config
— it needs the site key (DOM attribute), the environment (build-time
constant) and the ip-mode flags (DOM attributes). The entry now starts it
as soon as it has read those, and customDetectBot claims the in-flight
promise rather than issuing its own.

Loaded by dynamic import so the provider selector and API client stay out
of the entry chunk; entry grows ~400 bytes. Fire-and-forget: a failed
prefetch is indistinguishable from no prefetch and the existing path still
resolves a provider itself, so this cannot turn a working session into a
broken one.

The cache is single-use and keyed on (environment, ipMode, siteKey). A
retry is retrying because the pinned pronode failed, so it must not reuse
that pin; and a second widget with different flags must not claim another
widget's assignment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJ7KiTDKiu3mxQ4iuLFK2y
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJ7KiTDKiu3mxQ4iuLFK2y
@HughParry
HughParry merged commit fa4fedb into main Aug 4, 2026
11 checks passed
@HughParry
HughParry deleted the perf/parallel-detector-assign branch August 4, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant