Skip to content

perf(procaptcha-bundle): collapse the i18n stack into one chunk - #2972

Draft
HughParry wants to merge 1 commit into
mainfrom
perf/flatten-chunk-waterfall
Draft

perf(procaptcha-bundle): collapse the i18n stack into one chunk#2972
HughParry wants to merge 1 commit into
mainfrom
perf/flatten-chunk-waterfall

Conversation

@HughParry

Copy link
Copy Markdown
Contributor

Left to split automatically, i18n forms the longest serial chain in the widget's module graph. Measured on a staging demo load with headless Chromium — each level is a round trip, because the browser cannot discover the next module until the previous one has parsed:

 466 ms  procaptcha.bundle.js
 632 ms  commonChunk / web2Chunk / theme / language
 727 ms  rolldown-runtime / utilChunk
 751 ms  translations        ┐
 877 ms  i18nFrontend        │ four round trips
 919 ms  i18next             │ for one thing
 983 ms  translation.json    ┘
1029 ms  captchaRenderer
1057 ms  ProviderApi          <- the chunk that issues detector/assign
1513 ms  detector/assign starts

The widget needs all of i18n before it can render a label, so splitting it buys nothing and costs four round trips on the critical path. One chunk is one round trip.

This is the third of three changes against the same measurement (staging 4928 ms vs production 1744 ms to a clickable checkbox); the others are #2971 and prosopo/captcha-private#4042.

Verification status — please read

The build succeeds and the chunks collapse as intended (i18nChunk-*.js replaces translations, i18nFrontend, i18next, translation.json).

I have not verified runtime behaviour locally — my static-server harness failed to serve the bundle and I did not get a clean load test. That matters here specifically: the util-crypto rule immediately below this one documents a previous chunk merge in this graph producing an evaluation cycle, which presented as "init_dist is not a function" and the widget dying on load — not as a build error.

Cypress exercises the built bundle in a real browser, so it is the actual gate for this change. If it goes green this is safe; if it fails on widget init, that is the cycle and the merge needs narrowing (e.g. keeping translation.json separate).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XJ7KiTDKiu3mxQ4iuLFK2y

Left to split automatically, i18n forms the longest serial chain in the
widget's module graph. Measured on a staging demo load:

  751 ms  translations
  877 ms  i18nFrontend
  919 ms  i18next
  983 ms  translation.json
 1029 ms  captchaRenderer
 1057 ms  ProviderApi      <- the chunk that issues detector/assign

Each level is a round trip, because the browser cannot discover the next
module until the previous one has parsed. The widget needs all of i18n
before it can render a label, so the split buys nothing and costs four
round trips.

Note the util-crypto rule immediately below: merging chunks has caused
evaluation cycles in this graph before, which present as the widget dying
on load rather than as a build error. Cypress exercises the built bundle
in a real browser, so it is the gate for this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJ7KiTDKiu3mxQ4iuLFK2y
@HughParry
HughParry marked this pull request as draft August 4, 2026 08:46
@HughParry

Copy link
Copy Markdown
Contributor Author

Cypress confirms the risk flagged in the description — this change breaks widget load. Moving to draft.

CypressError: `cy.then()` timed out after waiting 4000ms
  "Warning: procaptcha script did not load within the expected time"
3 of 3 failed (100%)   captcha.cy.ts, correct.captcha.cy.ts, correct.captcha.signup.cy.ts

All three fail in before each, waiting for the procaptcha script that never initialises. That is the same failure mode the util-crypto rule documents immediately below this one: merging chunks in this graph produces an evaluation cycle where one chunk reads another's module-scope bindings before they are assigned. It kills the widget at load and does not fail the build, which is why the build passing and the chunks collapsing correctly told us nothing.

The measurement motivating this stands — i18n is four serial round trips (751 → 877 → 919 → 983 ms) directly ahead of ProviderApi, the chunk that issues detector/assign. It is worth ~300–500 ms if it can be done safely.

Next step for whoever picks this up: the merge is almost certainly too broad. Try narrowing it rather than abandoning it — e.g. group only the i18next runtime and leave @prosopo/locale / translation.json on their own, or preload the chain with <link rel="modulepreload"> instead of merging, which flattens the round trips without changing chunk evaluation order at all. The modulepreload route carries none of the cycle risk and is probably the better first attempt.

The other two changes from the same investigation are unaffected and green: #2971 and prosopo/captcha-private#4042.

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