Skip to content

fix: hint the core runtime in the head and measure pre-boot clicks - #1281

Draft
vivek7405 wants to merge 1 commit into
mainfrom
fix/core-modulepreload-preboot
Draft

fix: hint the core runtime in the head and measure pre-boot clicks#1281
vivek7405 wants to merge 1 commit into
mainfrom
fix/core-modulepreload-preboot

Conversation

@vivek7405

Copy link
Copy Markdown
Collaborator

Closes #1118

Links are clickable from first paint, but the boot is a <script type="module">,
which the HTML spec defers until parsing finishes. A click inside that window is
a plain browser navigation: a full document load on a site whose router is
supposed to soft-navigate. Measured on the live site, the router starts
listening at 25 to 35 ms warm and 313 ms cold on 300 kbps / 200 ms RTT.

The window cannot be closed from inside the router, so this shrinks it and
measures what is left. No inline capture shim (see the Rejected section on the
issue: no prior art in Turbo, zero reproductions in 56 measured clicks, and it
would fork the eligibility rules that live in onClick).

Shrinking it

Every module the boot imports pulls @webjsdev/core, but the boot script names
only page and component URLs, so the browser discovered core one full round trip
into the load. wrapHead now emits one <link rel="modulepreload"> for it,
ahead of the page and component hints since all of them import it.

  • The href is the importmap target verbatim, not run through fp(). The
    map's targets are already base-path-prefixed and content-hashed; a differing
    href makes the browser treat the preload and the import as two resources and
    fetch core twice, silently. This copies the vendor loop, not the module loop.
  • Gated on opts.moduleUrls.length || lazyEntries, so a fully elided page is
    not handed a preload for a runtime it never loads (App-module modulepreload over-fetches a dropped page's SSR-only subtree #780). That also keeps it
    off global-error.{js,ts}, whose document is returned verbatim.
  • vendorPreloadTargets is unchanged and still excludes core. A test pins that
    seam, since "fixing" the exclusion there would reintroduce the double-fetch.

Measuring what is left

A document that arrived by a same-origin navigation the router did not
soft-navigate reports pre-boot-navigation through the existing
webjs:navigation-fallback channel (#1114), willReload: false because the
load already happened. reportFallback leaves a sessionStorage marker naming
the destination when it chooses a full load, and the next boot consumes it, so a
degradation the router already reported under its own cause is not counted
twice. A reload, a back/forward restore and an external or typed entry are
excluded. The predicate is extracted pure and exported for unit testing.

Deliberately imprecise, and the docs say so: a data-no-router link, a
target="_blank" open and a cross-document form post land here too, and a fully
elided page reports nothing since the report rides the router's own boot. The
signal is the rate, not any single event.

Verification

  • e2e (headline), WEBJS_E2E=1: the served page carries exactly one core
    modulepreload and the browser issues exactly ONE request for that URL across
    a cold load, which is what proves byte-identity rather than eyeballing the
    tag. Second: a listener installed with evaluateOnNewDocument (nothing
    in-page can register before the boot module) sees pre-boot-navigation with
    willReload: false on a same-origin location.href load, and sees nothing on
    the entry load, which is the control against a router that reports every load.
  • Counterfactual: reverting ssr.js alone reds the preload assertion and
    leaves the event one green; reverting router-client.js alone reds the event
    assertion and leaves the preload one green.
  • Unit: the head assertions in vendor-preload.test.js (one core hint, href
    equal to the importmap target, none on an elided page, vendorPreloadTargets
    still excludes core); the predicate cases in router-client.test.js
    (navigate + same-origin referrer true; reload, back_forward,
    cross-origin, empty and unparseable referrer, and a matching marker all
    false; a stale marker for another href does not suppress).
  • Bun parity (required: the hook matches /ssr\.js): new pair
    test/bun/core-modulepreload.{mjs,test.mjs}, passing on Node and on Bun.
  • Full node suite 3859/3859, router browser suite 135/135 on Chromium, Firefox
    and WebKit.

Docs

website/app/docs/client-router/page.ts and
.agents/skills/webjs/references/client-router-and-streaming.md: the new cause
added to both enumerated lists, plus what it means, why the head hint exists,
and the stated imprecision.

Merge order: this lands SECOND, after #1252 (PR #1279). Both touch
router-client.js in disjoint regions; #1252 is a correctness bug on a shipped
feature, this is a load-time and observability improvement.

Links are clickable from first paint, but the boot is a module script, which
the HTML spec defers until parsing finishes. A click inside that window is a
plain browser navigation. The window cannot be closed from inside the router (an
inline capture shim has no prior art, forks the eligibility rules in onClick,
and adds an inline script to every page), so it is shrunk and then measured.

Shrinking it: every module the boot imports pulls @webjsdev/core, but the boot
script names only page and component URLs, so the browser discovered core one
full round trip late. The head now hints it, with the href taken straight from
the importmap target rather than through fp(), since the map's targets are
already base-path-prefixed and content-hashed and a differing href would make
the browser fetch core twice. Gated on the page actually shipping a boot module,
so a fully elided page is not handed a preload for a runtime it never loads.
vendorPreloadTargets still excludes core: the hint comes from the head builder,
not the vendor path.

Measuring what is left: a document that arrived by a same-origin navigation the
router did not soft-navigate now reports pre-boot-navigation through the
existing webjs:navigation-fallback channel, with willReload false because the
load already happened. A marker in sessionStorage keeps a full load the router
itself chose from being counted twice. A reload, a back/forward restore and an
external entry are excluded. The check is deliberately imprecise (a
data-no-router link and a cross-document form post land here too) and the docs
say so: the signal is the rate, not any single event.
@vivek7405 vivek7405 self-assigned this Aug 5, 2026
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.

dogfood: links are clickable before the client router intercepts, so an early click full-loads

1 participant