Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f933043
feat(skip): enable proven static layout transport skips
NathanDrake2406 May 22, 2026
6bbc788
fix(skip): wire client reuse manifests into app page rendering
NathanDrake2406 May 22, 2026
370e5d8
fix(skip): keep partial RSC payloads out of shared caches
NathanDrake2406 May 22, 2026
58dfd28
fix(skip): send client reuse manifests during navigation
NathanDrake2406 May 22, 2026
deff83f
fix(skip): keep partial RSC payloads client-scoped
NathanDrake2406 May 22, 2026
e7ba059
fix(skip): require exact compatibility for transport omission
NathanDrake2406 May 22, 2026
d9e8205
Merge remote-tracking branch 'upstream/main' into nathan/726-skip-04-…
NathanDrake2406 May 22, 2026
60f57f0
fix(skip): gate static layout skip on param observations
NathanDrake2406 May 22, 2026
1e5d1e6
fix(skip): reject param-scoped static layout skips
NathanDrake2406 May 22, 2026
b04d9b7
fix(skip): use structural layout param scope
NathanDrake2406 May 22, 2026
e1aef58
fix(skip): reject data-dependent static layout transport skips
NathanDrake2406 May 22, 2026
7bbf954
fix(skip): block finite-revalidate layout skips
NathanDrake2406 May 22, 2026
7702c68
fix(skip): probe wrapped layout server components
NathanDrake2406 May 22, 2026
6de6236
fix(skip): isolate layout probe dependency tracking
NathanDrake2406 May 22, 2026
d0004bd
fix(skip): cap browser reuse manifests to verification budget
NathanDrake2406 May 22, 2026
1863f07
fix(skip): reject request-api layout reuse proofs
NathanDrake2406 May 23, 2026
61bd846
fix(skip): avoid consuming iterable layout children
NathanDrake2406 May 23, 2026
26ae459
fix(skip): classify unsafe layout observations as dynamic
NathanDrake2406 May 23, 2026
8b4713b
fix(skip): probe layouts with non-null children
NathanDrake2406 May 23, 2026
fcaac5d
fix(skip): reject unstable cache layout dependencies
NathanDrake2406 May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ export default __createAppRscHandler({
configRedirects: __configRedirects,
configRewrites: __configRewrites,
dispatchMatchedPage({
clientReuseManifest,
cleanPathname,
formState,
actionError,
Expand Down Expand Up @@ -547,6 +548,7 @@ export default __createAppRscHandler({
renderMode,
});
},
clientReuseManifest,
cleanPathname,
clearRequestContext() {
__clearRequestContext();
Expand Down
32 changes: 26 additions & 6 deletions packages/vinext/src/server/app-elements-wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
CacheProofRejectionCode,
RenderObservation,
} from "./cache-proof.js";
import type { ClientReuseManifestSkipDisposition } from "./client-reuse-manifest.js";
import { isInterceptionMatchedUrlPath } from "./normalize-path.js";

const APP_INTERCEPTION_SEPARATOR = "\0";
Expand Down Expand Up @@ -260,6 +261,7 @@ type AppElementsWireCodec = {
cacheEntryReuseProof?: CacheEntryReuseProof;
layoutFlags: LayoutFlags;
renderObservation?: RenderObservation;
skipDisposition?: ClientReuseManifestSkipDisposition;
}): ReactNode | AppOutgoingElements;
encodePageId(routePath: string, interceptionContext: string | null): string;
encodeRouteId(routePath: string, interceptionContext: string | null): string;
Expand Down Expand Up @@ -605,10 +607,12 @@ export function buildOutgoingAppPayload(input: {
cacheEntryReuseProof?: CacheEntryReuseProof;
layoutFlags: LayoutFlags;
renderObservation?: RenderObservation;
skipDisposition?: ClientReuseManifestSkipDisposition;
}): ReactNode | AppOutgoingElements {
if (!isAppElementsRecord(input.element)) {
return input.element;
}
const skippedLayoutIds = createSkippedLayoutIds(input.skipDisposition);
const payload: Record<
string,
| ReactNode
Expand All @@ -618,12 +622,14 @@ export function buildOutgoingAppPayload(input: {
| AppElementsInterception
| RenderObservation
| readonly AppElementsSlotBinding[]
> = {
...input.element,
[APP_LAYOUT_FLAGS_KEY]: input.layoutFlags,
[APP_ARTIFACT_COMPATIBILITY_KEY]:
input.artifactCompatibility ?? createArtifactCompatibilityEnvelope(),
};
> = {};
for (const [key, value] of Object.entries(input.element)) {
if (skippedLayoutIds.has(key)) continue;
payload[key] = value;
}
payload[APP_LAYOUT_FLAGS_KEY] = input.layoutFlags;
payload[APP_ARTIFACT_COMPATIBILITY_KEY] =
input.artifactCompatibility ?? createArtifactCompatibilityEnvelope();
if (input.cacheEntryReuseProof) {
payload[APP_CACHE_ENTRY_REUSE_PROOF_KEY] = input.cacheEntryReuseProof;
}
Expand All @@ -633,6 +639,20 @@ export function buildOutgoingAppPayload(input: {
return payload;
}

function createSkippedLayoutIds(
skipDisposition: ClientReuseManifestSkipDisposition | undefined,
): ReadonlySet<string> {
if (skipDisposition?.enabled !== true) return new Set();
Comment thread
NathanDrake2406 marked this conversation as resolved.
Outdated

const skippedLayoutIds = new Set<string>();
for (const id of skipDisposition.skippedEntryIds) {
if (parseAppElementsWireElementKey(id)?.kind === "layout") {
skippedLayoutIds.add(id);
}
}
return skippedLayoutIds;
}

function readArtifactCompatibilityMetadata(value: unknown): ArtifactCompatibilityEnvelope {
if (value === undefined) return createArtifactCompatibilityEnvelope();

Expand Down
3 changes: 3 additions & 0 deletions packages/vinext/src/server/app-page-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
} from "./app-rsc-render-mode.js";
import { createAppPageTreePath } from "./app-page-route-wiring.js";
import type { AppPageSsrHandler } from "./app-page-stream.js";
import type { ClientReuseManifestParseResult } from "./client-reuse-manifest.js";
import { createStaticGenerationHeadersContext } from "./app-static-generation.js";
import { buildPageCacheTags } from "./implicit-tags.js";
import type { ISRCacheEntry } from "./isr-cache.js";
Expand Down Expand Up @@ -152,6 +153,7 @@ type DispatchAppPageOptions<TRoute extends AppPageDispatchRoute> = {
opts: AppPageDispatchInterceptOptions | undefined,
searchParams: URLSearchParams,
) => Promise<AppPageElement>;
clientReuseManifest?: ClientReuseManifestParseResult;
cleanPathname: string;
clearRequestContext: () => void;
createRscOnErrorHandler: (pathname: string, routePath: string) => AppPageBoundaryOnError;
Expand Down Expand Up @@ -634,6 +636,7 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
return options.createRscOnErrorHandler(pathname, routePath);
},
element: pageBuildResult.element,
clientReuseManifest: options.clientReuseManifest,
getDraftModeCookieHeader,
getFontLinks: options.getFontLinks,
getFontPreloads: options.getFontPreloads,
Expand Down
232 changes: 221 additions & 11 deletions packages/vinext/src/server/app-page-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ import {
createArtifactCompatibilityGraphVersion,
type ArtifactCompatibilityEnvelope,
} from "./artifact-compatibility.js";
import {
buildCacheVariantWithRouteBudget,
buildRenderObservation,
buildRenderRequestApiObservations,
createStaticLayoutArtifactReuseDecision,
DEFAULT_CACHE_VARIANT_BUDGET,
type StaticLayoutCacheProofOutputScope,
} from "./cache-proof.js";
import type {
ClientReuseManifestParseResult,
ClientReuseManifestSkipDisposition,
} from "./client-reuse-manifest.js";
import { NO_STORE_CACHE_CONTROL } from "./cache-control.js";
import {
createClientReuseSkipTransportPlan,
createStaticLayoutClientReuseArtifactCompatibility,
createStaticLayoutClientReusePayloadHash,
crossCheckClientReuseManifestEntryWithCache,
} from "./skip-cache-proof.js";
import {
createAppPageHtmlOutputScope,
createAppPageRenderObservation,
Expand Down Expand Up @@ -129,6 +148,8 @@ type RenderAppPageLifecycleOptions = {
routePattern: string;
runWithSuppressedHookWarning<T>(probe: () => Promise<T>): Promise<T>;
scriptNonce?: string;
clientReuseManifest?: ClientReuseManifestParseResult;
skipDisposition?: ClientReuseManifestSkipDisposition;
mountedSlotsHeader?: string | null;
renderMode?: AppRscRenderMode;
waitUntil?: (promise: Promise<void>) => void;
Expand Down Expand Up @@ -212,6 +233,175 @@ function createAppPageArtifactCompatibility(
});
}

const STATIC_LAYOUT_SKIP_VERIFICATION_ENTRY_BUDGET = 8;
Comment thread
NathanDrake2406 marked this conversation as resolved.
Outdated

function readStringMetadata(
element: Readonly<Record<string, unknown>>,
key: string,
): string | null {
const value = element[key];
return typeof value === "string" ? value : null;
}

function createStaticLayoutOutputScope(input: {
artifactCompatibility: ArtifactCompatibilityEnvelope;
element: Readonly<Record<string, unknown>>;
layoutId: string;
}): StaticLayoutCacheProofOutputScope | null {
const routeId = readStringMetadata(input.element, AppElementsWire.keys.route);
if (routeId === null) return null;

return {
kind: "layout",
layoutId: input.layoutId,
rootBoundaryId: input.artifactCompatibility.rootBoundaryId,
routeId,
};
}

function createRenderLifecycleSkipDisposition(input: {
artifactCompatibility: ArtifactCompatibilityEnvelope | undefined;
cleanPathname: string;
clientReuseManifest: ClientReuseManifestParseResult | undefined;
element: ReactNode | Readonly<Record<string, ReactNode>>;
isRscRequest: boolean;
layoutFlags: Readonly<Record<string, "s" | "d">>;
routePattern: string;
}): ClientReuseManifestSkipDisposition | undefined {
Comment thread
NathanDrake2406 marked this conversation as resolved.
if (!input.isRscRequest || input.clientReuseManifest === undefined) {
return undefined;
}
if (!isAppElementsRecord(input.element) || input.artifactCompatibility === undefined) {
Comment thread
NathanDrake2406 marked this conversation as resolved.
return createClientReuseSkipTransportPlan({
manifest: input.clientReuseManifest,
maxEntriesToVerify: 0,
verifyEntry(entry) {
return crossCheckClientReuseManifestEntryWithCache({
artifact: {
compatibility: createArtifactCompatibilityEnvelope(),
invalidation: { kind: "unknown" },
payloadHash: null,
},
cacheDecision: null,
entry,
});
},
}).skipDisposition;
}
const element = input.element;
const artifactCompatibility = input.artifactCompatibility;

const staticLayoutIds = new Set(
Object.entries(input.layoutFlags)
.filter(([, flag]) => flag === "s")
.map(([layoutId]) => layoutId),
);
Comment thread
NathanDrake2406 marked this conversation as resolved.
const plan = createClientReuseSkipTransportPlan({
manifest: input.clientReuseManifest,
maxEntriesToVerify: STATIC_LAYOUT_SKIP_VERIFICATION_ENTRY_BUDGET,
verifyEntry(entry) {
if (
entry.kind !== "layout" ||
!staticLayoutIds.has(entry.id) ||
AppElementsWire.parseElementKey(entry.id)?.kind !== "layout"
) {
return crossCheckClientReuseManifestEntryWithCache({
artifact: {
compatibility: artifactCompatibility,
invalidation: { kind: "unknown" },
payloadHash: null,
},
cacheDecision: null,
entry,
});
}

const output = createStaticLayoutOutputScope({
artifactCompatibility,
element,
layoutId: entry.id,
});
if (output === null) {
return crossCheckClientReuseManifestEntryWithCache({
artifact: {
compatibility: artifactCompatibility,
invalidation: { kind: "unknown" },
payloadHash: null,
},
cacheDecision: null,
entry,
});
}

const candidateVariant = buildCacheVariantWithRouteBudget({
budget: DEFAULT_CACHE_VARIANT_BUDGET,
dimensions: [],
output,
routeBudget: {
routeId: output.routeId,
variantCacheKeys: [],
},
});
const skipArtifactCompatibility =
candidateVariant.kind === "variant"
? createStaticLayoutClientReuseArtifactCompatibility({
artifactCompatibility,
layoutId: entry.id,
rootBoundaryId: output.rootBoundaryId,
routeId: output.routeId,
variantCacheKey: candidateVariant.variant.cacheKey,
})
: artifactCompatibility;
const cacheDecision = createStaticLayoutArtifactReuseDecision({
candidateArtifactCompatibility: skipArtifactCompatibility,
candidateObservation: buildRenderObservation({
Comment thread
NathanDrake2406 marked this conversation as resolved.
boundaryOutcome: { kind: "success" },
cacheability: "public",
cacheTags: [],
completeness: "complete",
dynamicFetches: [],
output,
pathTags: [input.cleanPathname],
requestApis: buildRenderRequestApiObservations({
completeness: "complete",
observed: [],
}),
}),
Comment thread
NathanDrake2406 marked this conversation as resolved.
candidateVariant,
currentArtifactCompatibility: skipArtifactCompatibility,
currentOutput: output,
});

return crossCheckClientReuseManifestEntryWithCache({
artifact: {
compatibility: skipArtifactCompatibility,
invalidation: { kind: "valid" },
payloadHash:
candidateVariant.kind === "variant"
? createStaticLayoutClientReusePayloadHash({
artifactCompatibility: skipArtifactCompatibility,
layoutId: entry.id,
rootBoundaryId: output.rootBoundaryId,
routeId: output.routeId,
variantCacheKey: candidateVariant.variant.cacheKey,
})
: null,
},
cacheDecision,
entry,
});
},
});

return plan.skipDisposition;
}

function isSkipTransportEnabled(
skipDisposition: ClientReuseManifestSkipDisposition | undefined,
): boolean {
return skipDisposition?.enabled === true;
}

/**
* Wraps an RSC response body to report invalid dynamic usage errors after the
* stream is fully consumed. In dev mode, errors from cookies()/headers() inside
Expand Down Expand Up @@ -349,11 +539,25 @@ export async function renderAppPageLifecycle(
params: options.params,
state: options.peekRenderObservationState?.() ?? createEmptyAppPageRenderObservationState(),
});
const skipDisposition =
options.skipDisposition ??
createRenderLifecycleSkipDisposition({
artifactCompatibility,
cleanPathname: options.cleanPathname,
clientReuseManifest: options.clientReuseManifest,
element: options.element,
isRscRequest: options.isRscRequest,
layoutFlags,
routePattern: options.routePattern,
});
const shouldBypassRscCacheForSkipTransport =
options.isRscRequest && isSkipTransportEnabled(skipDisposition);
const outgoingElement = AppElementsWire.encodeOutgoingPayload({
element: options.element,
layoutFlags,
...(artifactCompatibility ? { artifactCompatibility } : {}),
renderObservation: payloadRenderObservation,
skipDisposition: options.isRscRequest ? skipDisposition : undefined,
});

const compileEnd = options.isProduction ? undefined : performance.now();
Expand All @@ -379,7 +583,8 @@ export async function renderAppPageLifecycle(
(options.isProduction || options.isPrerender === true) &&
(revalidateSeconds === null || (revalidateSeconds > 0 && revalidateSeconds !== Infinity)) &&
!options.isDraftMode &&
!options.isForceDynamic;
!options.isForceDynamic &&
!shouldBypassRscCacheForSkipTransport;
const rscCapture = teeAppPageRscStreamForCapture(rscStream, shouldCaptureRscForCacheMetadata);
const rscForResponse = rscCapture.ssrStream;

Expand All @@ -404,16 +609,21 @@ export async function renderAppPageLifecycle(
}

const dynamicUsedDuringBuild = options.consumeDynamicUsage();
const rscResponsePolicy = resolveAppPageRscResponsePolicy({
dynamicUsedDuringBuild,
isDraftMode: options.isDraftMode,
isDynamicError: options.isDynamicError,
isForceDynamic: options.isForceDynamic,
isForceStatic: options.isForceStatic,
isProduction: options.isProduction,
expireSeconds,
revalidateSeconds,
});
const rscResponsePolicy = shouldBypassRscCacheForSkipTransport
? { cacheControl: NO_STORE_CACHE_CONTROL }
Comment thread
NathanDrake2406 marked this conversation as resolved.
: resolveAppPageRscResponsePolicy({
dynamicUsedDuringBuild,
isDraftMode: options.isDraftMode,
isDynamicError: options.isDynamicError,
isForceDynamic: options.isForceDynamic,
isForceStatic: options.isForceStatic,
isProduction: options.isProduction,
expireSeconds,
revalidateSeconds,
});
if (shouldBypassRscCacheForSkipTransport) {
options.isrDebug?.("RSC cache write skipped (skip transport payload)", options.cleanPathname);
}
const rscResponse = buildAppPageRscResponse(rscForResponse, {
middlewareContext: options.middlewareContext,
mountedSlotsHeader: options.mountedSlotsHeader,
Expand Down
Loading
Loading