fix(prerender): static metadata route should be optimized by default - #3004
fix(prerender): static metadata route should be optimized by default#3004NriotHrreion wants to merge 11 commits into
Conversation
commit: |
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 546a9abfa0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7facc418cf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| !rendered.response.ok || | ||
| !isMetadataResponseCacheable(rendered.response) || | ||
| !isUseCacheFunction(functions.defaultExport) || | ||
| !isRenderedMetadataRouteCacheable(rendered) || |
There was a problem hiding this comment.
Keep private metadata responses out of the shared cache
After removing the "use cache" gate, a regular metadata function that returns Cache-Control: private is now admitted to the outer ISR cache because isMetadataResponseCacheable rejects only no-cache and no-store. A later HIT rebuilds the response with shared ISR cache-control, replacing the original private policy and potentially serving request-specific metadata to other users; reject private responses before prerendering or writing them to ISR.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Beyond the scope of this PR
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 336df7006f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // `export const dynamic = "force-dynamic"` forces the route to be dynamic. | ||
| if (Reflect.get(module, "dynamic") === "force-dynamic") return true; | ||
|
|
||
| // `export const revalidate = 0` means "never cache", | ||
| // so treat it the same as force-dynamic. | ||
| return Reflect.get(module, "revalidate") === 0 || dynamicDetected; |
There was a problem hiding this comment.
Apply static dynamic configs during metadata rendering
When a metadata module exports dynamic = "force-static" and calls headers() or cookies(), the metadata handler never installs the force-static headers context used by configureAppRouteStaticGenerationContext; the isolated detector therefore reports dynamic usage and the prerender is skipped instead of receiving empty request data and producing a static artifact. The related dynamic = "error" mode is also treated like auto, silently skipping rather than raising the required static-generation error. Configure these two modes around metadata execution as the App Route path does.
AGENTS.md reference: AGENTS.md:L174-L181
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Maybe beyond the scope of this PR
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 878366416c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // `export const dynamic = "force-dynamic"` forces the route to be dynamic. | ||
| if (Reflect.get(module, "dynamic") === "force-dynamic") return true; | ||
|
|
||
| // `export const revalidate = 0` means "never cache", | ||
| // so treat it the same as force-dynamic. | ||
| return Reflect.get(module, "revalidate") === 0 || dynamicDetected; |
There was a problem hiding this comment.
Bypass cached entries for explicitly dynamic metadata routes
When a deployment changes a previously cached metadata route to dynamic = "force-dynamic" or revalidate = 0, this predicate prevents new writes and prerendering but does not prevent readMatchedPrerenderedMetadataRouteResponse() from returning the existing persistent ISR entry before the route executes. The route can therefore continue serving the old static response—and a stale entry indefinitely—despite explicitly opting out of caching; apply the same dynamic decision before attempting the cache read.
AGENTS.md reference: AGENTS.md:L174-L181
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Beyond the scope of this PR
| const response = route.isDynamic | ||
| ? await callDynamicMetadataRoute(route, match, options.makeThenableParams, functions) | ||
| : serveStaticMetadataRoute(route); | ||
| return captureRenderedMetadataRoute(response); | ||
| }); |
There was a problem hiding this comment.
Observe dynamic usage while consuming streaming responses
When a metadata export returns a lazy ImageResponse or Response(stream) whose producer calls headers(), cookies(), noStore(), or establishes cache lifetime while the body is pulled, this isolated scope ends as soon as the Response object is returned. buildAppRouteCacheValue() consumes the body afterward, so that late usage is recorded only in the parent context and rendered.dynamic remains false, allowing request-dependent bytes to be prerendered or written to shared ISR; keep observation active through body materialization or propagate observations made during consumption.
AGENTS.md reference: AGENTS.md:L194-L198
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Beyond the scope of this PR
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@james-elicx Ready for a review! |
Closes #2950
Overview
Generated metadata images such as
opengraph-image.tsxshould be statically optimized by default when they can be rendered without request-time dependencies, matching Next.js behavior.Previously, vinext tied metadata prerender eligibility to the callable
"use cache"marker. This conflated function-level React Flight caching with route-level response caching, and made"use cache"appear necessary even for otherwise staticImageResponseroutes.This PR fixes the issue by treating generated metadata routes as prerender candidates by default and determining their cacheability from actual runtime dynamic usage. Routes that use request-time APIs such as
headers()or explicitly opt into dynamic rendering remain dynamic and are not persisted as prerendered artifacts.What changed
isUseCacheFunction()is removed, and no longer determining the cacheability with"use cache".runWithIsolatedDynamicUsage()to detect dynamic operations in the route, such asawait headers(). Then thedynamicDetectedis used as a factor to determine the cacheability.isMetadataRouteDynamic()here is fordynamic = "force-dynamic"andrevalidate = 0:captureRenderedMetadataRoute(), cache control header will be set ifdynamicUsageis true.getPrerenderableMetadataRoutePaths(), generated image IDs are enumerated into paths with ID and pushed into thepaths, just as the generated sitemaps do.Testing
og-image-optimizationpnpm test tests/metadata-route-response.test.ts