Skip to content

Generated metadata images are not statically optimized by default #2950

Description

@MaxtuneLee

Summary

In vinext@1.0.0-beta.6, a generated metadata image is considered a prerender candidate only when its default export has vinext's USE_CACHE_FUNCTION_SYMBOL. In practice, that marker is added by the callable function-level "use cache" transform.

This makes "use cache" appear necessary to prerender a static opengraph-image.tsx, even though generated metadata images in Next.js are statically optimized by default when they do not use dynamic inputs.

"use cache" is not a safe workaround for a route returning ImageResponse: it puts the Response through the callable React Flight cache, where it is not a valid return value.

Environment

  • vinext: 1.0.0-beta.6
  • @vitejs/plugin-rsc: 0.5.34
  • React / React DOM / react-server-dom-webpack: 19.2.7
  • Vite: 8.1.3
  • Node.js: 22.13.1
  • pnpm: 11.1.1

Next.js reference used for behavior and source comparison:

  • Next.js: 16.3.0-canary.105
  • commit: ccd47cfe53c7fef5e6b7ad472a3a020605a23608

Reproduction

https://github.com/MaxtuneLee/vinext-image-response-use-cache-repro

Create a static generated metadata image without "use cache":

// app/opengraph-image.tsx
import { ImageResponse } from "next/og";

export const size = { width: 1200, height: 630 };

export default function OpenGraphImage() {
  return new ImageResponse(
    <div style={{ display: "flex", width: "100%", height: "100%" }}>
      static metadata image
    </div>,
    size,
  );
}

Run:

pnpm vinext build --prerender-all

Inspect dist/server/vinext-prerender.json and dist/server/prerendered-routes/.

Actual behavior

The generated metadata route is excluded from metadata prerender candidates unless its default export has Symbol.for("vinext.useCacheFunction"). A normal static opengraph-image.tsx has no such marker because it does not use the callable "use cache" transform.

As a result, a route that is otherwise static does not get the metadata response prerender artifact solely because it has no callable-cache wrapper.

Adding "use cache" makes it eligible, but changes the execution path: the ImageResponse is then passed to the callable React Flight serializer. That is a separate incorrect behavior, not a valid way to opt into metadata response caching.

Expected behavior (Next.js behavior)

Next.js statically optimizes generated opengraph-image.tsx routes by default. A static-eligible route does not need "use cache"; Next.js creates it at build time and serves it from the route response cache.

It becomes dynamic only when its code uses request-time APIs, uncached data, or explicit dynamic route configuration. This is documented in the Next.js opengraph-image file-convention documentation.

vinext should similarly:

  • consider a generated metadata image as a metadata/App Route prerender candidate without requiring a callable-cache marker;
  • use route dynamic-usage and cacheability state to decide whether it can be persisted;
  • store the returned HTTP Response body, status, and headers as the metadata response artifact.

Possible root cause

packages/vinext/src/server/metadata-route-response.ts uses Symbol.for("vinext.useCacheFunction") on the metadata default export as the eligibility test in getPrerenderableMetadataRoutePaths().

The symbol indicates that the function went through packages/vinext/src/plugins/use-cache-callable.ts. It is therefore a proxy for a particular implementation detail, not evidence that a metadata route is static or response-cacheable.

This conflates two independent concerns:

  1. whether the metadata route can be rendered at build time and its HTTP response persisted;
  2. whether the function's JavaScript return value is eligible for callable React Flight caching.

An ImageResponse is valid for the first concern and invalid for the second.

Suggested fix

  1. Remove USE_CACHE_FUNCTION_SYMBOL as a prerequisite for metadata route prerender eligibility and metadata response caching.
  2. Send generated metadata routes through the normal metadata/App Route prerender path, then use dynamic-usage and cacheability signals to determine whether to write an artifact.
  3. Keep buildAppRouteCacheValue(response) as the response-cache representation for generated images.
  4. Add an integration test for an opengraph-image.tsx with no "use cache" that asserts a 200 response artifact is written during vinext build --prerender-all.
  5. Add a dynamic metadata-image test to confirm request-time APIs or uncached data still prevent static prerendering.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions