mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
89c6799257
Under Cache Components, metadata image routes such as `opengraph-image` and `icon` that return an `ImageResponse` were always rendered on demand (`ƒ`) rather than prerendered. `ImageResponse` defers rasterizing its element tree into the response body stream, and the route-handler prerender unwraps that body within a single task; the rasterization never finishes within that budget, so the route was classified as dynamic. This change renders and caches the image during the prerender so these routes become static (`○`). During the prerender we serialize the `ImageResponse` arguments with React Flight's `prerenderToNodeStream` inside the prerender work-unit store, which runs the user's component tree once in the correct scope. This brings any user-space I/O inside that tree, such as `cookies()` or an uncached `fetch`, under the same Cache Components rules that already governed I/O elsewhere in the handler. The hanging-input abort signal bounds the serialization and decides static versus dynamic: if the tree is still waiting on dynamic input once the prerender's cache-sourced input is ready, the serialization can't complete and we return a hanging promise, so the final prerender's macrotask budget classifies the route as dynamic; a tree that resolves entirely from static data or `use cache` finishes serializing and is rendered to an image. This mirrors `encryptActionBoundArgs`, which serializes server action bound args with React Flight under the same hanging-input abort signal during a prerender. The fully resolved element tree is then handed to satori. Because React Flight encodes an async Server Component's output as a `React.lazy` that satori can't walk, those references are resolved into plain elements first; this lets an async server component, including one that uses `use cache`, be passed as the `ImageResponse` element. Rasterization runs outside the prerender work-unit store: inside a Cache Components prerender an uncached `fetch`, such as the renderer loading a font, is turned into a hanging promise (Cache Components skips I/O that would not be cached anyway), so running satori with no store lets those framework fetches resolve normally. Crucially, because satori only walks the already-resolved tree, no user component runs in that storeless scope, so uncached user-space I/O can't be wrongly allowed there and let a route that should be dynamic render as static. The rendered image is stored as an `ArrayBuffer` in a new in-memory `imageResponses` store on the Resume Data Cache, keyed by a base64 encoding of its serialized arguments, and the cache signal is held open until it is stored so the prospective prerender waits for it. The final prerender retrieves the array buffer from memory within microtasks. The store is in-memory only and is never serialized, so the image array buffers never enter the resume data that ships with the prerender. The caching path lives in a separate `cache-image-response` module that is loaded only for Cache Components builds, gated behind `process.env.__NEXT_CACHE_COMPONENTS` so the `require` and its React Flight dependencies are eliminated as dead code otherwise; apps without Cache Components keep `ImageResponse`'s original streaming behavior unchanged. --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
36 lines
825 B
TypeScript
36 lines
825 B
TypeScript
import { cookies } from 'next/headers'
|
|
import { ImageResponse } from 'next/og'
|
|
|
|
export const contentType = 'image/png'
|
|
export const alt = 'Twitter'
|
|
export const size = { width: 1600, height: 900 }
|
|
|
|
export default function twitterImage() {
|
|
return new ImageResponse(<AsyncTwitterImage />, size)
|
|
}
|
|
|
|
async function AsyncTwitterImage() {
|
|
const cookieStore = await cookies()
|
|
const numCookies = cookieStore.getAll().length
|
|
|
|
const data = await fetch(
|
|
'https://next-data-api-endpoint.vercel.app/api/random'
|
|
).then((res) => res.text())
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: 128,
|
|
background: 'lavender',
|
|
}}
|
|
>
|
|
{data} {numCookies}
|
|
</div>
|
|
)
|
|
}
|