Files
Hendrik LiebauGitHubvercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
89c6799257 Statically prerender metadata image routes under Cache Components (#94957)
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>
2026-06-22 19:38:45 +02:00

34 lines
673 B
TypeScript

import { ImageResponse } from 'next/og'
export const contentType = 'image/png'
export const size = { width: 180, height: 180 }
export default function appleIcon() {
return new ImageResponse(<AsyncAppleIcon />)
}
async function AsyncAppleIcon() {
'use cache'
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: 88,
background: '#fff',
color: '#000',
}}
>
{data}
</div>
)
}