Skip to content

pendingMinMs restarts at hydration for a server-rendered pending component #8648

Open

Description

@Tarekkharsa

AI disclosure: Claude (Opus) investigated this and wrote this issue.

Which project does this relate to?

Router

Describe the bug

Follow-up to #8640, as asked in #8640 (comment). This is only about how long loaded content waits after hydration, not about the duplicate pending component.

A ssr: 'data-only' / ssr: false route server-renders its pendingComponent, so the pending UI is visible from first paint. On hydration, offerPending starts pendingMinMs from now:

presentedPending ? Date.now() + min : tx[4 /* startedAt */] + delay,

"Now" is after the client JS has loaded. On a slow connection the pending UI has already been visible longer than the minimum, and the content still waits the full minimum again.

Not questioning the server fallback itself (#5122).

Complete minimal reproducer

https://github.com/Tarekkharsa/tanstack-router-pending-min-after-hydration-repro

Steps to Reproduce the Bug

pnpm i && pnpm test: builds, serves vite preview, loads /dashboard (instant loader) in Chromium, with JS served normally and with every /assets/*.js held 600 ms.

Expected behavior

Pending UI visible for at least pendingMinMs in total, then content as soon as code and data are ready.

Screenshots or Videos

1.170.41, pendingMinMs: 400, three runs each:

JS held skeleton painted code + data ready content wait after ready
0 ms ~16 ms ~50 ms ~455 ms ~405 ms
600 ms ~16 ms ~650 ms ~1060 ms ~405 ms

With pendingMinMs: 0 the wait after ready is ~3 ms.

Platform

  • @tanstack/react-router 1.170.41, @tanstack/react-start 1.168.60
  • macOS, Chromium (Playwright 1.62.1), vite 8.3.1

Additional context

Constraint, from the reverted #8643 (#8647): in React, if the minimum has run out when hydrate() resolves, the load can commit success before React hydrates the route's Suspense boundary. The pending branch must then still render during hydration.

Tried locally, measurement only, on top of #8643 plus a hydration check in MatchInner:

-        presentedPending ? Date.now() + min : tx[4 /* startedAt */] + delay,
+        tx[4 /* startedAt */] + (presentedPending ? min : delay),
 ...
-    Date.now(),
+    resolvedPrefix ? Date.now() - performance.now() : Date.now(),

The hydration transaction then starts at navigation start. With JS held, content came ~3 ms after it was ready (12/12 loads). Caveat: navigation start is before first paint, so a fast load shows the pending UI slightly less than the minimum (~390 of 400 ms warm, ~290 ms cold).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions