Skip to content

fix: collapse React to a single optimized chunk to prevent duplicate React on cold start#132

Merged
uhyo merged 1 commit into
masterfrom
fix/issue-128-single-react-chunk
Jun 29, 2026
Merged

fix: collapse React to a single optimized chunk to prevent duplicate React on cold start#132
uhyo merged 1 commit into
masterfrom
fix/issue-128-single-react-chunk

Conversation

@uhyo

@uhyo uhyo commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #128 — on a cold-started dev server (no .vite cache), React was loaded twice on the very first request, producing an Invalid hook call before Vite auto-reloaded. Also fixes the related fsRoutes-without-ssr:true dev failure (#124), where the eval'd dev-JSX could not resolve react/jsx-runtime (does not provide an export named 't').

This is an alternative to #129 that makes the failure mode structurally impossible rather than merely avoided. See the root-cause investigation on #128 for the full analysis.

Root cause (one bug, two symptoms)

A dependency that @vitejs/plugin-rsc reaches through a request-time client-package-proxy virtual module (e.g. @funstack/router) is invisible to the dependency optimizer's initial scan, so it is only discovered on the first render. That late discovery triggers a re-optimization pass which, mid-flight, corrupts CJS-interop module references:

Both vanish on reload because everything then agrees on the post-re-optimization state — hence the cold-start-only, first-render-only nature.

Fix

Two coordinated changes:

  1. Pre-bundle the project's client packages (direct deps that declare react as a peerDependency — the same heuristic plugin-rsc uses) in the browser environment, so they are discovered in the optimizer's initial pass and no re-optimization happens on the first render. (findClientPackages)
  2. Use bare react/react-dom instead of the nested @funstack/static > react form. The bare specifiers merge with the copy the scanner already discovers, so React is bundled into a single optimized chunk — making the duplicate-React flip impossible even if a re-optimization ever slips through.

Why both: bare React alone can't ship — it regresses #124 (that's why the nested form was added in #126). Pre-discovery fixes #124's real cause; the single chunk makes #128 impossible by construction. This is strictly stronger than #129, which keeps the redundant second React chunk and only avoids the flip.

Notes:

  • Scoped to direct dependencies (bare specifiers a server component can import and Vite can resolve from the project root). @funstack/static (excluded from optimizeDeps) and react-dom (handled separately) are skipped. The react-in-peerDependencies key excludes build plugins like @vitejs/plugin-react (peer-depends on vite), so no false positives.
  • The previous hardcoded reliance on the nested React form is removed; client packages are auto-detected in all entry modes.

Verification

Fixes #128

🤖 Generated with Claude Code

…React on cold start

On a cold-started dev server (no `.vite` cache), React was loaded twice on the
very first request, producing an `Invalid hook call` error before Vite
auto-reloaded the page (#128). Built-in file-system routing without `ssr: true`
hit a related failure where the eval'd dev-JSX could not resolve
`react/jsx-runtime` (`does not provide an export named 't'`, #124).

Both are the same underlying problem: a dependency that `@vitejs/plugin-rsc`
reaches through a request-time `client-package-proxy` virtual module (e.g.
`@funstack/router`) is invisible to the optimizer's initial scan, so it is only
discovered on the first render. That late discovery triggers a re-optimization
pass which, mid-flight, corrupts CJS-interop module references — leaving a
second copy of React loaded (#128) or a stale dev-JSX runtime (#124).

Two coordinated changes:

- Pre-bundle the project's directly-declared client packages (deps that list
  `react` as a peer dependency — the same heuristic plugin-rsc uses) in the
  browser environment, so they are discovered in the optimizer's initial pass
  and no re-optimization happens on the first render. See findClientPackages.

- Include React/ReactDOM as bare `react`/`react-dom` entries instead of the
  nested `@funstack/static > react` form. The bare specifiers merge with the
  copy the optimizer scanner already discovers, so React is bundled into a
  single optimized chunk. The nested form produced a second, redundant React
  chunk from the same source file; whenever a re-optimization changed which
  chunk was canonical, two live React instances ended up loaded. One chunk
  makes that structurally impossible.

Verified: multiple cold starts are clean (single React chunk, no
re-optimization, no auto-reload) for fsRoutes/`@funstack/router` with zero user
config; full unit suite, dev e2e (24), and build e2e (27) pass across
single-entry, multi-entry, ssr-defer, and fs-routing modes.

Fixes #128

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@uhyo uhyo merged commit b9dc9b8 into master Jun 29, 2026
2 checks passed
@uhyo uhyo deleted the fix/issue-128-single-react-chunk branch June 29, 2026 13:38
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.

React loads twice in first load from cold started dev server File-system routing requires ssr: true for the dev server (lift this limitation)

1 participant