From a9223b6ed9d6a51464f7ee9cd42924ed600f8b42 Mon Sep 17 00:00:00 2001
From: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Date: Thu, 9 Apr 2026 21:26:27 +0800
Subject: [PATCH 1/3] add test
---
.../layout-search-params.spec.ts | 60 +++++++++++++++++++
.../[id]/layout-shell.tsx | 42 +++++++++++++
.../layout-search-params/[id]/layout.tsx | 17 ++++++
.../layout-search-params/[id]/page.tsx | 14 +++++
.../layout-search-params/layout.tsx | 5 ++
5 files changed, 138 insertions(+)
create mode 100644 tests/e2e/app-router/nextjs-compat/layout-search-params.spec.ts
create mode 100644 tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout-shell.tsx
create mode 100644 tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout.tsx
create mode 100644 tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/page.tsx
create mode 100644 tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/layout.tsx
diff --git a/tests/e2e/app-router/nextjs-compat/layout-search-params.spec.ts b/tests/e2e/app-router/nextjs-compat/layout-search-params.spec.ts
new file mode 100644
index 000000000..c9de35996
--- /dev/null
+++ b/tests/e2e/app-router/nextjs-compat/layout-search-params.spec.ts
@@ -0,0 +1,60 @@
+/**
+ * Next.js compat: layout state across search param changes.
+ *
+ * Based on Next.js: test/e2e/app-dir/search-params-react-key/layout-params.test.ts
+ * https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/search-params-react-key/layout-params.test.ts
+ *
+ * Extends the same expectation to a parent client layout rendered by a server layout:
+ * query-only push/replace should not remount that layout.
+ */
+
+import { expect, test } from "@playwright/test";
+import { waitForAppRouterHydration } from "../../helpers";
+
+const BASE = "http://localhost:4174";
+
+test.describe("Next.js compat: layout state across search param changes", () => {
+ test("router.push() keeps parent client layout mounted on query-only navigation", async ({
+ page,
+ }) => {
+ await page.goto(`${BASE}/nextjs-compat/layout-search-params/demo`);
+ await waitForAppRouterHydration(page);
+
+ await page.click("#layout-increment");
+ await page.click("#layout-increment");
+ await expect(page.locator("#layout-count")).toHaveText("2");
+ await expect(page.locator("#layout-mount-count")).toHaveText("1");
+
+ await page.click("#layout-push");
+
+ await expect(async () => {
+ expect(page.url()).toContain("foo=bar");
+ }).toPass({ timeout: 10_000 });
+
+ await expect(page.locator("#search-params")).toContainText('"foo":"bar"');
+ await expect(page.locator("#layout-count")).toHaveText("2");
+ await expect(page.locator("#layout-mount-count")).toHaveText("1");
+ });
+
+ test("router.replace() keeps parent client layout mounted on query-only navigation", async ({
+ page,
+ }) => {
+ await page.goto(`${BASE}/nextjs-compat/layout-search-params/demo`);
+ await waitForAppRouterHydration(page);
+
+ await page.click("#layout-increment");
+ await page.click("#layout-increment");
+ await expect(page.locator("#layout-count")).toHaveText("2");
+ await expect(page.locator("#layout-mount-count")).toHaveText("1");
+
+ await page.click("#layout-replace");
+
+ await expect(async () => {
+ expect(page.url()).toContain("foo=baz");
+ }).toPass({ timeout: 10_000 });
+
+ await expect(page.locator("#search-params")).toContainText('"foo":"baz"');
+ await expect(page.locator("#layout-count")).toHaveText("2");
+ await expect(page.locator("#layout-mount-count")).toHaveText("1");
+ });
+});
diff --git a/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout-shell.tsx b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout-shell.tsx
new file mode 100644
index 000000000..97388c4a6
--- /dev/null
+++ b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout-shell.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+
+declare global {
+ interface Window {
+ __vinextLayoutSearchParamsMountCount__?: number;
+ }
+}
+
+function LayoutShellInner({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const [count, setCount] = useState(0);
+ const [mountCount, setMountCount] = useState(0);
+
+ useEffect(() => {
+ window.__vinextLayoutSearchParamsMountCount__ =
+ (window.__vinextLayoutSearchParamsMountCount__ ?? 0) + 1;
+ setMountCount(window.__vinextLayoutSearchParamsMountCount__);
+ }, []);
+
+ return (
+
+
Layout Search Params
+
{count}
+
{mountCount}
+
+
+
+ {children}
+
+ );
+}
+
+export const LayoutShell = React.memo(LayoutShellInner);
diff --git a/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout.tsx b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout.tsx
new file mode 100644
index 000000000..1f0864871
--- /dev/null
+++ b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout.tsx
@@ -0,0 +1,17 @@
+import { LayoutShell } from "./layout-shell";
+
+export default async function LayoutSearchParamsLayout({
+ children,
+ params,
+}: {
+ children: React.ReactNode;
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+ return (
+
+ {children}
+ {id}
+
+ );
+}
diff --git a/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/page.tsx b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/page.tsx
new file mode 100644
index 000000000..be885279b
--- /dev/null
+++ b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/page.tsx
@@ -0,0 +1,14 @@
+export default async function LayoutSearchParamsPage({
+ searchParams,
+}: {
+ searchParams: Promise>;
+}) {
+ const params = await searchParams;
+
+ return (
+
+
{JSON.stringify(params)}
+
Query-only navigation should preserve parent layout state.
+
+ );
+}
diff --git a/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/layout.tsx b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/layout.tsx
new file mode 100644
index 000000000..f48091142
--- /dev/null
+++ b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/layout.tsx
@@ -0,0 +1,5 @@
+"use client";
+
+export default function LayoutSearchParamsOuterLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>;
+}
From 7693c176efc002236d8af9c94543c633e99efc62 Mon Sep 17 00:00:00 2001
From: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Date: Thu, 9 Apr 2026 21:26:32 +0800
Subject: [PATCH 2/3] code
---
.../src/server/app-browser-client-loader.ts | 202 ++++++++++++++++++
.../vinext/src/server/app-browser-entry.ts | 30 ++-
2 files changed, 227 insertions(+), 5 deletions(-)
create mode 100644 packages/vinext/src/server/app-browser-client-loader.ts
diff --git a/packages/vinext/src/server/app-browser-client-loader.ts b/packages/vinext/src/server/app-browser-client-loader.ts
new file mode 100644
index 000000000..eb795874c
--- /dev/null
+++ b/packages/vinext/src/server/app-browser-client-loader.ts
@@ -0,0 +1,202 @@
+import { createElement, forwardRef } from "react";
+import { setRequireModule } from "@vitejs/plugin-rsc/core/browser";
+import * as clientReferences from "virtual:vite-rsc/client-references";
+import type { CachedRscResponse } from "../shims/navigation.js";
+
+declare const __vite_rsc_raw_import__: (id: string) => Promise;
+
+type TrackedPromise = Promise & {
+ reason?: unknown;
+ status?: "fulfilled" | "pending" | "rejected";
+ value?: T;
+};
+
+type MemoLikeValue = {
+ $$typeof: symbol;
+ displayName?: string;
+ type?: unknown;
+};
+
+const CLIENT_REFERENCE_ROW_PATTERN = /(?:^|\n)[0-9a-fA-F]+:I\[("(?:\\.|[^"\\])*")/g;
+const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
+const REACT_MEMO_TYPE = Symbol.for("react.memo");
+const textDecoder = new TextDecoder();
+const clientModuleCache = new Map>();
+const stableMemoExportCache = new Map();
+const stableModuleExportCache = new Map();
+let installed = false;
+
+function withTrailingSlash(path: string): string {
+ return path[path.length - 1] === "/" ? path : `${path}/`;
+}
+
+function normalizeClientReferenceId(id: string): string {
+ return id.split("$$cache=")[0];
+}
+
+function trackPromise(promise: Promise): TrackedPromise {
+ const tracked = promise as TrackedPromise;
+ if (tracked.status) {
+ return tracked;
+ }
+
+ tracked.status = "pending";
+ promise.then(
+ (value) => {
+ tracked.status = "fulfilled";
+ tracked.value = value;
+ },
+ (reason) => {
+ tracked.status = "rejected";
+ tracked.reason = reason;
+ },
+ );
+ return tracked;
+}
+
+function isObjectRecord(value: unknown): value is Record {
+ return !!value && typeof value === "object";
+}
+
+function isForwardRefLikeValue(value: unknown): value is { $$typeof: symbol } {
+ return isObjectRecord(value) && "$$typeof" in value && value.$$typeof === REACT_FORWARD_REF_TYPE;
+}
+
+function isMemoLikeValue(value: unknown): value is MemoLikeValue {
+ return isObjectRecord(value) && "$$typeof" in value && value.$$typeof === REACT_MEMO_TYPE;
+}
+
+function createStableMemoProxy(
+ normalizedId: string,
+ exportName: string,
+ value: MemoLikeValue,
+): unknown {
+ const cacheKey = `${normalizedId}#${exportName}`;
+ const cached = stableMemoExportCache.get(cacheKey);
+ if (cached) {
+ return cached;
+ }
+
+ const target = value as unknown as Parameters[0];
+ const supportsRef = isForwardRefLikeValue(value.type);
+
+ const proxy = supportsRef
+ ? forwardRef>(function VinextStableMemoProxy(props, ref) {
+ return createElement(target, Object.assign({}, props, { ref }) as never);
+ })
+ : function VinextStableMemoProxy(props: Record) {
+ return createElement(target, props);
+ };
+
+ const namedProxy = proxy as { displayName?: string };
+ namedProxy.displayName =
+ value.displayName ??
+ (typeof value.type === "function" && value.type.name ? value.type.name : exportName);
+
+ stableMemoExportCache.set(cacheKey, namedProxy);
+ return namedProxy;
+}
+
+function stabilizeClientModuleExports(normalizedId: string, moduleExports: unknown): unknown {
+ const cached = stableModuleExportCache.get(normalizedId);
+ if (cached) {
+ return cached;
+ }
+
+ if (isMemoLikeValue(moduleExports)) {
+ const stable = createStableMemoProxy(normalizedId, "default", moduleExports);
+ stableModuleExportCache.set(normalizedId, stable);
+ return stable;
+ }
+
+ if (
+ !moduleExports ||
+ (typeof moduleExports !== "object" && typeof moduleExports !== "function")
+ ) {
+ stableModuleExportCache.set(normalizedId, moduleExports);
+ return moduleExports;
+ }
+
+ let changed = false;
+ const next = Object.create(Object.getPrototypeOf(moduleExports));
+ const descriptors = Object.getOwnPropertyDescriptors(moduleExports);
+
+ for (const [exportName, descriptor] of Object.entries(descriptors)) {
+ if ("value" in descriptor && isMemoLikeValue(descriptor.value)) {
+ descriptor.value = createStableMemoProxy(normalizedId, exportName, descriptor.value);
+ changed = true;
+ }
+ Object.defineProperty(next, exportName, descriptor);
+ }
+
+ const stableModuleExports = changed ? next : moduleExports;
+ stableModuleExportCache.set(normalizedId, stableModuleExports);
+ return stableModuleExports;
+}
+
+function loadClientReference(normalizedId: string): Promise {
+ if (!import.meta.env.__vite_rsc_build__) {
+ return __vite_rsc_raw_import__(
+ withTrailingSlash(import.meta.env.BASE_URL) + normalizedId.slice(1),
+ ).then((moduleExports) => stabilizeClientModuleExports(normalizedId, moduleExports));
+ }
+
+ const importReference = clientReferences.default[normalizedId] as
+ | (() => Promise)
+ | undefined;
+ if (!importReference) {
+ throw new Error(`client reference not found '${normalizedId}'`);
+ }
+ return importReference().then((moduleExports) =>
+ stabilizeClientModuleExports(normalizedId, moduleExports),
+ );
+}
+
+function getTrackedClientModule(id: string): TrackedPromise {
+ const normalizedId = normalizeClientReferenceId(id);
+ const cached = clientModuleCache.get(normalizedId);
+ if (cached) {
+ return cached;
+ }
+
+ const tracked = trackPromise(Promise.resolve().then(() => loadClientReference(normalizedId)));
+ clientModuleCache.set(normalizedId, tracked);
+ return tracked;
+}
+
+export function installVinextBrowserClientLoader(): void {
+ if (installed) {
+ return;
+ }
+
+ installed = true;
+ setRequireModule({
+ load(id) {
+ return getTrackedClientModule(id);
+ },
+ });
+}
+
+export async function prewarmClientReferencesFromSnapshot(
+ snapshot: CachedRscResponse,
+): Promise {
+ const payload = textDecoder.decode(snapshot.buffer);
+ const pending: Promise[] = [];
+
+ CLIENT_REFERENCE_ROW_PATTERN.lastIndex = 0;
+ let match: RegExpExecArray | null;
+ while ((match = CLIENT_REFERENCE_ROW_PATTERN.exec(payload))) {
+ try {
+ const id = JSON.parse(match[1]) as string;
+ pending.push(getTrackedClientModule(id));
+ } catch {
+ // Ignore malformed rows and let the regular RSC decoder surface errors.
+ }
+ }
+
+ if (pending.length === 0) {
+ return;
+ }
+
+ await Promise.allSettled(pending);
+}
diff --git a/packages/vinext/src/server/app-browser-entry.ts b/packages/vinext/src/server/app-browser-entry.ts
index dd74e35e8..d4c55be35 100644
--- a/packages/vinext/src/server/app-browser-entry.ts
+++ b/packages/vinext/src/server/app-browser-entry.ts
@@ -16,7 +16,7 @@ import {
createTemporaryReferenceSet,
encodeReply,
setServerCallback,
-} from "@vitejs/plugin-rsc/browser";
+} from "@vitejs/plugin-rsc/react/browser";
import { hydrateRoot } from "react-dom/client";
import "../client/instrumentation-client.js";
import { notifyAppRouterTransitionStart } from "../client/instrumentation-client-state.js";
@@ -46,6 +46,10 @@ import {
createProgressiveRscStream,
getVinextBrowserGlobal,
} from "./app-browser-stream.js";
+import {
+ installVinextBrowserClientLoader,
+ prewarmClientReferencesFromSnapshot,
+} from "./app-browser-client-loader.js";
type SearchParamInput = ConstructorParameters[0];
@@ -488,11 +492,15 @@ async function readInitialRscStream(): Promise> {
restoreHydrationNavigationContext(window.location.pathname, window.location.search, params);
- if (!rscResponse.body) {
+ const responseSnapshot = await snapshotRscResponse(rscResponse);
+ await prewarmClientReferencesFromSnapshot(responseSnapshot);
+
+ const restoredResponse = restoreRscResponse(responseSnapshot, false);
+ if (!restoredResponse.body) {
throw new Error("[vinext] Initial RSC response had no body");
}
- return rscResponse.body;
+ return restoredResponse.body;
}
function registerServerActionCallback(): void {
@@ -534,8 +542,10 @@ function registerServerActionCallback(): void {
clearClientNavigationCaches();
+ const responseSnapshot = await snapshotRscResponse(fetchResponse);
+ await prewarmClientReferencesFromSnapshot(responseSnapshot);
const result = await createFromFetch(
- Promise.resolve(fetchResponse),
+ Promise.resolve(restoreRscResponse(responseSnapshot)),
{ temporaryReferences },
);
@@ -573,6 +583,7 @@ function registerServerActionCallback(): void {
}
async function main(): Promise {
+ installVinextBrowserClientLoader();
registerServerActionCallback();
const rscStream = await readInitialRscStream();
@@ -642,6 +653,7 @@ async function main(): Promise {
// wrapping only) — no stale-navigation recheck needed between here and the
// next await.
const cachedNavigationSnapshot = createClientNavigationRenderSnapshot(href, cachedParams);
+ await prewarmClientReferencesFromSnapshot(cachedRoute.response);
const cachedPayload = await createFromFetch(
Promise.resolve(restoreRscResponse(cachedRoute.response)),
);
@@ -726,6 +738,10 @@ async function main(): Promise {
if (navId !== activeNavigationId) return;
+ await prewarmClientReferencesFromSnapshot(responseSnapshot);
+
+ if (navId !== activeNavigationId) return;
+
const rscPayload = await createFromFetch(
Promise.resolve(restoreRscResponse(responseSnapshot)),
);
@@ -801,8 +817,12 @@ async function main(): Promise {
import.meta.hot.on("rsc:update", async () => {
try {
clearClientNavigationCaches();
+ const responseSnapshot = await snapshotRscResponse(
+ await fetch(toRscUrl(window.location.pathname + window.location.search)),
+ );
+ await prewarmClientReferencesFromSnapshot(responseSnapshot);
const rscPayload = await createFromFetch(
- fetch(toRscUrl(window.location.pathname + window.location.search)),
+ Promise.resolve(restoreRscResponse(responseSnapshot)),
);
// HMR updates skip renderNavigationPayload — no snapshot activated.
updateBrowserTree(
From 5edf6cc7ba0bb88582ec8a7b201930d23539bb84 Mon Sep 17 00:00:00 2001
From: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Date: Tue, 14 Apr 2026 16:48:24 +0800
Subject: [PATCH 3/3] Revert "code"
This reverts commit 7693c176efc002236d8af9c94543c633e99efc62.
---
.../src/server/app-browser-client-loader.ts | 202 ------------------
.../vinext/src/server/app-browser-entry.ts | 30 +--
2 files changed, 5 insertions(+), 227 deletions(-)
delete mode 100644 packages/vinext/src/server/app-browser-client-loader.ts
diff --git a/packages/vinext/src/server/app-browser-client-loader.ts b/packages/vinext/src/server/app-browser-client-loader.ts
deleted file mode 100644
index eb795874c..000000000
--- a/packages/vinext/src/server/app-browser-client-loader.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-import { createElement, forwardRef } from "react";
-import { setRequireModule } from "@vitejs/plugin-rsc/core/browser";
-import * as clientReferences from "virtual:vite-rsc/client-references";
-import type { CachedRscResponse } from "../shims/navigation.js";
-
-declare const __vite_rsc_raw_import__: (id: string) => Promise;
-
-type TrackedPromise = Promise & {
- reason?: unknown;
- status?: "fulfilled" | "pending" | "rejected";
- value?: T;
-};
-
-type MemoLikeValue = {
- $$typeof: symbol;
- displayName?: string;
- type?: unknown;
-};
-
-const CLIENT_REFERENCE_ROW_PATTERN = /(?:^|\n)[0-9a-fA-F]+:I\[("(?:\\.|[^"\\])*")/g;
-const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
-const REACT_MEMO_TYPE = Symbol.for("react.memo");
-const textDecoder = new TextDecoder();
-const clientModuleCache = new Map>();
-const stableMemoExportCache = new Map();
-const stableModuleExportCache = new Map();
-let installed = false;
-
-function withTrailingSlash(path: string): string {
- return path[path.length - 1] === "/" ? path : `${path}/`;
-}
-
-function normalizeClientReferenceId(id: string): string {
- return id.split("$$cache=")[0];
-}
-
-function trackPromise(promise: Promise): TrackedPromise {
- const tracked = promise as TrackedPromise;
- if (tracked.status) {
- return tracked;
- }
-
- tracked.status = "pending";
- promise.then(
- (value) => {
- tracked.status = "fulfilled";
- tracked.value = value;
- },
- (reason) => {
- tracked.status = "rejected";
- tracked.reason = reason;
- },
- );
- return tracked;
-}
-
-function isObjectRecord(value: unknown): value is Record {
- return !!value && typeof value === "object";
-}
-
-function isForwardRefLikeValue(value: unknown): value is { $$typeof: symbol } {
- return isObjectRecord(value) && "$$typeof" in value && value.$$typeof === REACT_FORWARD_REF_TYPE;
-}
-
-function isMemoLikeValue(value: unknown): value is MemoLikeValue {
- return isObjectRecord(value) && "$$typeof" in value && value.$$typeof === REACT_MEMO_TYPE;
-}
-
-function createStableMemoProxy(
- normalizedId: string,
- exportName: string,
- value: MemoLikeValue,
-): unknown {
- const cacheKey = `${normalizedId}#${exportName}`;
- const cached = stableMemoExportCache.get(cacheKey);
- if (cached) {
- return cached;
- }
-
- const target = value as unknown as Parameters[0];
- const supportsRef = isForwardRefLikeValue(value.type);
-
- const proxy = supportsRef
- ? forwardRef>(function VinextStableMemoProxy(props, ref) {
- return createElement(target, Object.assign({}, props, { ref }) as never);
- })
- : function VinextStableMemoProxy(props: Record) {
- return createElement(target, props);
- };
-
- const namedProxy = proxy as { displayName?: string };
- namedProxy.displayName =
- value.displayName ??
- (typeof value.type === "function" && value.type.name ? value.type.name : exportName);
-
- stableMemoExportCache.set(cacheKey, namedProxy);
- return namedProxy;
-}
-
-function stabilizeClientModuleExports(normalizedId: string, moduleExports: unknown): unknown {
- const cached = stableModuleExportCache.get(normalizedId);
- if (cached) {
- return cached;
- }
-
- if (isMemoLikeValue(moduleExports)) {
- const stable = createStableMemoProxy(normalizedId, "default", moduleExports);
- stableModuleExportCache.set(normalizedId, stable);
- return stable;
- }
-
- if (
- !moduleExports ||
- (typeof moduleExports !== "object" && typeof moduleExports !== "function")
- ) {
- stableModuleExportCache.set(normalizedId, moduleExports);
- return moduleExports;
- }
-
- let changed = false;
- const next = Object.create(Object.getPrototypeOf(moduleExports));
- const descriptors = Object.getOwnPropertyDescriptors(moduleExports);
-
- for (const [exportName, descriptor] of Object.entries(descriptors)) {
- if ("value" in descriptor && isMemoLikeValue(descriptor.value)) {
- descriptor.value = createStableMemoProxy(normalizedId, exportName, descriptor.value);
- changed = true;
- }
- Object.defineProperty(next, exportName, descriptor);
- }
-
- const stableModuleExports = changed ? next : moduleExports;
- stableModuleExportCache.set(normalizedId, stableModuleExports);
- return stableModuleExports;
-}
-
-function loadClientReference(normalizedId: string): Promise {
- if (!import.meta.env.__vite_rsc_build__) {
- return __vite_rsc_raw_import__(
- withTrailingSlash(import.meta.env.BASE_URL) + normalizedId.slice(1),
- ).then((moduleExports) => stabilizeClientModuleExports(normalizedId, moduleExports));
- }
-
- const importReference = clientReferences.default[normalizedId] as
- | (() => Promise)
- | undefined;
- if (!importReference) {
- throw new Error(`client reference not found '${normalizedId}'`);
- }
- return importReference().then((moduleExports) =>
- stabilizeClientModuleExports(normalizedId, moduleExports),
- );
-}
-
-function getTrackedClientModule(id: string): TrackedPromise {
- const normalizedId = normalizeClientReferenceId(id);
- const cached = clientModuleCache.get(normalizedId);
- if (cached) {
- return cached;
- }
-
- const tracked = trackPromise(Promise.resolve().then(() => loadClientReference(normalizedId)));
- clientModuleCache.set(normalizedId, tracked);
- return tracked;
-}
-
-export function installVinextBrowserClientLoader(): void {
- if (installed) {
- return;
- }
-
- installed = true;
- setRequireModule({
- load(id) {
- return getTrackedClientModule(id);
- },
- });
-}
-
-export async function prewarmClientReferencesFromSnapshot(
- snapshot: CachedRscResponse,
-): Promise {
- const payload = textDecoder.decode(snapshot.buffer);
- const pending: Promise[] = [];
-
- CLIENT_REFERENCE_ROW_PATTERN.lastIndex = 0;
- let match: RegExpExecArray | null;
- while ((match = CLIENT_REFERENCE_ROW_PATTERN.exec(payload))) {
- try {
- const id = JSON.parse(match[1]) as string;
- pending.push(getTrackedClientModule(id));
- } catch {
- // Ignore malformed rows and let the regular RSC decoder surface errors.
- }
- }
-
- if (pending.length === 0) {
- return;
- }
-
- await Promise.allSettled(pending);
-}
diff --git a/packages/vinext/src/server/app-browser-entry.ts b/packages/vinext/src/server/app-browser-entry.ts
index d4c55be35..dd74e35e8 100644
--- a/packages/vinext/src/server/app-browser-entry.ts
+++ b/packages/vinext/src/server/app-browser-entry.ts
@@ -16,7 +16,7 @@ import {
createTemporaryReferenceSet,
encodeReply,
setServerCallback,
-} from "@vitejs/plugin-rsc/react/browser";
+} from "@vitejs/plugin-rsc/browser";
import { hydrateRoot } from "react-dom/client";
import "../client/instrumentation-client.js";
import { notifyAppRouterTransitionStart } from "../client/instrumentation-client-state.js";
@@ -46,10 +46,6 @@ import {
createProgressiveRscStream,
getVinextBrowserGlobal,
} from "./app-browser-stream.js";
-import {
- installVinextBrowserClientLoader,
- prewarmClientReferencesFromSnapshot,
-} from "./app-browser-client-loader.js";
type SearchParamInput = ConstructorParameters[0];
@@ -492,15 +488,11 @@ async function readInitialRscStream(): Promise> {
restoreHydrationNavigationContext(window.location.pathname, window.location.search, params);
- const responseSnapshot = await snapshotRscResponse(rscResponse);
- await prewarmClientReferencesFromSnapshot(responseSnapshot);
-
- const restoredResponse = restoreRscResponse(responseSnapshot, false);
- if (!restoredResponse.body) {
+ if (!rscResponse.body) {
throw new Error("[vinext] Initial RSC response had no body");
}
- return restoredResponse.body;
+ return rscResponse.body;
}
function registerServerActionCallback(): void {
@@ -542,10 +534,8 @@ function registerServerActionCallback(): void {
clearClientNavigationCaches();
- const responseSnapshot = await snapshotRscResponse(fetchResponse);
- await prewarmClientReferencesFromSnapshot(responseSnapshot);
const result = await createFromFetch(
- Promise.resolve(restoreRscResponse(responseSnapshot)),
+ Promise.resolve(fetchResponse),
{ temporaryReferences },
);
@@ -583,7 +573,6 @@ function registerServerActionCallback(): void {
}
async function main(): Promise {
- installVinextBrowserClientLoader();
registerServerActionCallback();
const rscStream = await readInitialRscStream();
@@ -653,7 +642,6 @@ async function main(): Promise {
// wrapping only) — no stale-navigation recheck needed between here and the
// next await.
const cachedNavigationSnapshot = createClientNavigationRenderSnapshot(href, cachedParams);
- await prewarmClientReferencesFromSnapshot(cachedRoute.response);
const cachedPayload = await createFromFetch(
Promise.resolve(restoreRscResponse(cachedRoute.response)),
);
@@ -738,10 +726,6 @@ async function main(): Promise {
if (navId !== activeNavigationId) return;
- await prewarmClientReferencesFromSnapshot(responseSnapshot);
-
- if (navId !== activeNavigationId) return;
-
const rscPayload = await createFromFetch(
Promise.resolve(restoreRscResponse(responseSnapshot)),
);
@@ -817,12 +801,8 @@ async function main(): Promise {
import.meta.hot.on("rsc:update", async () => {
try {
clearClientNavigationCaches();
- const responseSnapshot = await snapshotRscResponse(
- await fetch(toRscUrl(window.location.pathname + window.location.search)),
- );
- await prewarmClientReferencesFromSnapshot(responseSnapshot);
const rscPayload = await createFromFetch(
- Promise.resolve(restoreRscResponse(responseSnapshot)),
+ fetch(toRscUrl(window.location.pathname + window.location.search)),
);
// HMR updates skip renderNavigationPayload — no snapshot activated.
updateBrowserTree(