Skip to content

fix(website-tanstack): stop the pending flash and make intent preloading effective#480

Draft
IgnazioDS wants to merge 1 commit into
aymericzip:mainfrom
IgnazioDS:fix/462-router-pending-flash
Draft

fix(website-tanstack): stop the pending flash and make intent preloading effective#480
IgnazioDS wants to merge 1 commit into
aymericzip:mainfrom
IgnazioDS:fix/462-router-pending-flash

Conversation

@IgnazioDS

Copy link
Copy Markdown

Part of #462 - this addresses the pending-component flash and the blocked client-side transitions. The cold-load FCP axis (CSS delivery, bundle weight) is separate and not touched here.

The two config values that caused the navigation symptoms

defaultPendingMs: 0 entered the pending state on every navigation whose loader had not already resolved - and defaultPendingMinMs, which was not overridden, then held that pending frame for its 500ms framework default. Net effect: a minimum half-second blank screen on essentially every route change, even when the loader resolved in single-digit milliseconds. (Defaults confirmed in @tanstack/router-core src/router.ts: defaultPendingMs: 1000, defaultPendingMinMs: 500.) Removing the override restores the 1000ms threshold, so pending UI only appears for genuinely slow loads.

defaultPreloadStaleTime: 0 marked every intent/viewport preload stale by the time the user actually clicked, so the loader re-ran on navigation and blocked the transition - hover preloading was effectively disabled. The 0 setting is TanStack's recommendation only when loaders delegate caching to an external store (queryClient.ensureQueryData); the doc/blog loaders here call server functions directly, so the framework default (30s) is the correct behavior.

With both removed: links preloaded on intent/viewport resolve before the click, navigation commits instantly, and no pending frame appears. defaultStaleTime: Infinity already keeps visited routes instant.

Why this is only half of #462

First visits to a doc page still pay two server round-trips in the loader (loadDocPage + loadNavData). The real fix for that is the staticFunctionMiddleware you already wired (currently commented out in serverFunctions/docs.ts) once the build-time payload issue from TanStack/router#7630 is resolved - I am digging into that one separately. One data point already checked: @tanstack/react-start@1.168.25 resolves @tanstack/start-plugin-core@1.171.17, which already contains the TSS_CLIENT_OUTPUT_DIR fix from TanStack/router#6940, so the cause is something else.

…ing effective

Addresses the navigation half of aymericzip#462.

Two router options were fighting the framework:

- defaultPendingMs: 0 entered the pending state on every navigation
  whose loader had not already resolved, and defaultPendingMinMs (500ms
  framework default) then held that blank pending frame for at least
  half a second - the black-screen flash on each route change. Removing
  the override restores the 1000ms threshold, so pending UI only appears
  for genuinely slow loads.

- defaultPreloadStaleTime: 0 marked every intent/viewport preload stale
  by the time the user actually clicked, so the loader re-ran and
  blocked the transition. Hover preloading was effectively disabled.
  The 0 setting is meant for loaders that delegate caching to an
  external store (queryClient.ensureQueryData); the doc/blog loaders
  call server functions directly, so the framework default (30s) is
  the correct behavior here.

Verified against @tanstack/router-core defaults (defaultPendingMs: 1000,
defaultPendingMinMs: 500, preloadStaleTime default 30_000).
@aymericzip

Copy link
Copy Markdown
Owner

Hey @IgnazioDS

I have tested your fix that remove defaultPreloadStaleTime and defaultPendingMs, but still getting Unexpected token '<', "<!DOCTYPE "... is not valid JSON when .middleware([staticFunctionMiddleware]) are uncommented

@aymericzip aymericzip marked this pull request as draft July 12, 2026 21:19
@IgnazioDS

Copy link
Copy Markdown
Author

Hey @aymericzip — that is expected with the middleware enabled: this PR only fixes the navigation flash and the preload re-runs. The <!DOCTYPE error with staticFunctionMiddleware is a separate problem, and I root-caused it yesterday with your repro — full writeup here: TanStack/router#7630 (comment)

Short version:

  1. The middleware only writes payload files for server-function calls that actually execute during prerender. With crawlLinks: false, any route (and locale variant) that is not in prerender.pages gets no payload file. You can confirm after a build by checking .output/public/__tsr/staticServerFnCache/ — in the repro build that directory does not exist at all, because none of the prerendered pages invoke the doc server functions.

  2. At runtime the client half of the middleware fetches the payload URL and calls r.json() directly — no response.ok check, no content-type check, and no fallback to ctx.next() (staticFunctionMiddleware.js in @tanstack/start-static-server-functions@1.167.17).

  3. The missing payload does not even return a 404: the {-$locale}/$ catch-all serves the app shell as 200 text/html (it parses __tsr as a locale), so r.json() throws exactly Unexpected token '<', "<!DOCTYPE ".

So with the middleware on, any route/locale/args combination not covered by the prerender pass is guaranteed to crash on navigation. It is not the #6940 output-directory bug — your pinned react-start already resolves the fixed start-plugin-core 1.171.17.

What you can do today: make sure prerender.pages covers every route that calls a static-middleware function, including every locale variant (the payload hash includes the serialized args). Or keep the middleware commented out until upstream adds a graceful fallback — the fix I suggested on #7630 is to validate the response (ok + content-type) and fall back to ctx.next() when the payload is missing, so uncovered routes degrade to a live server call instead of crashing.

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.

2 participants