vercel / vercel/next.js

Runtime-rendered fully static pages on PPR routes are stored without .rsc and are never read back from the file-system cache (MISS forever)

Open
#98,306 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Cache Components
Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

Link to the code that reproduces this issue

https://github.com/Shtokarev/next-ppr-runtime-rsc-repro

To Reproduce
  1. npm install && npm run build && ./repro.sh runtime
    • The route is a plain /[slug] page with no runtime data, generateStaticParams returns built, cacheComponents: true. The build shows ◐ /[slug] and ○ /built.
    • next.config.js sets cacheMaxMemorySize: 0 so every read goes to the file-system cache. With the default 50 MB LRU the same happens as soon as the entry is evicted from memory.
  2. repro.sh starts next start, requests /runtime (not prerendered) three times and lists the files the cache wrote:
/runtime request 1 -> status=200 x-nextjs-cache=MISS
/runtime request 2 -> status=200 x-nextjs-cache=MISS
/runtime request 3 -> status=200 x-nextjs-cache=MISS
/built request   -> status=200 x-nextjs-cache=HIT
files written for /runtime:
  runtime.html
  runtime.meta
  runtime.segments
  1. node apply-fix.js && ./repro.sh runtime2 applies the one-line change proposed below to the installed next and repeats:
/runtime2 request 1 -> status=200 x-nextjs-cache=MISS
/runtime2 request 2 -> status=200 x-nextjs-cache=HIT
/runtime2 request 3 -> status=200 x-nextjs-cache=HIT
files written for /runtime2:
  runtime2.html
  runtime2.meta
  runtime2.rsc
  runtime2.segments
Current vs. Expected behavior

Current: a fully static page (no postponed state) rendered at runtime on a PPR-enabled route is persisted by FileSystemCache.set() as .html, .meta and .segments/ but without .rsc. FileSystemCache.get() requires .rsc for every APP_PAGE entry that has no postponed state, the readFile throws, the handler returns null, and the server treats the page as a cache miss: x-nextjs-cache: MISS and a full blocking render on every request once the entry is no longer in the in-memory LRU (or immediately with cacheMaxMemorySize: 0). Pages prerendered at build time are unaffected because the export step writes their .rsc.

Expected: a runtime-rendered entry is read back from disk like a build-time one (HIT/STALE), and client navigations to it are served from the stored flight data.

Where the three code paths disagree (16.3.4, same in 16.4.0-canary.19):

  • write, packages/next/src/server/lib/incremental-cache/file-system-cache.ts (set): .rsc is written only when !ctx.isRoutePPREnabled.
  • read, same file (get): .rsc is read when !ctx.isFallback && (!ctx.isRoutePPREnabled || meta.postponed == null).
  • build export, packages/next/src/export/routes/app-page.ts: shouldWriteRsc = !isRoutePPREnabled || (!postponed && !hasFallbackParams), with the comment "for fully static outputs (no postponed state and no fallback params), we can safely emit the route .rsc to support static navigations".

The read side and the export already follow the "fully static output" rule; the runtime write side still uses the older "PPR off" rule, so runtime-rendered fully static entries are never readable.

Proposed fix (one line in set()):

// before
if (!ctx.fetchCache && !ctx.isFallback && !ctx.isRoutePPREnabled) {
// after
if (!ctx.fetchCache && !ctx.isFallback && (!ctx.isRoutePPREnabled || data.postponed == null)) {

apply-fix.js in the repro applies exactly this change to the installed package (to the bundled copies in dist/compiled/next-server/*.runtime.prod.js, which is what the production server executes) and the entry becomes readable, as shown above.

Impact on self-hosted next start with cacheComponents: every page on a dynamic segment that is not enumerated at build time, or that is generated on a fresh pod, is effectively memory-only. With hundreds of such pages the LRU is exhausted in minutes and each first visit pays a blocking render. Related but different: #97457 / #97734 deal with the file-system cache returning null after revalidateTag(..., { expire: 0 }).

Provide environment information
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.6.0
  Available memory (MB): 49152
  Available CPU cores: 14
Binaries:
  Node: 24.15.0
  npm: 11.12.1
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 16.3.4
  eslint-config-next: N/A
  react: 19.2.3
  react-dom: 19.2.3
  typescript: 7.0.2
Next.js Config:
  output: N/A
Which area(s) are affected? (Select all that apply)

Partial Prerendering (PPR), Cache Components, Incremental Static Regeneration (ISR)

Which stage(s) are affected? (Select all that apply)

next start (local), Other (self-hosted Node.js server)

Additional context

The same behaviour was first observed in a production-like app with cacheComponents, where a startup crawler renders every route of every locale/device variant on the pod: the pages listed in the prerender manifest stayed HIT/STALE, while every runtime-rendered dynamic-segment page turned into MISS with a 150–1200 ms render as soon as the 50 MB LRU evicted it. Checked with NEXT_PRIVATE_DEBUG_CACHE=1 and by placing a .rsc file next to such an entry by hand: with the file present the same entry is served from disk.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Run the linked reproduction with npm install && npm run build && ./repro.sh runtime, then inspect packages/next/src/server/lib/incremental-cache/file-system-cache.ts and the export logic in packages/next/src/export/routes/app-page.ts. Verify that runtime-rendered fully static PPR entries write .rsc and subsequent requests return cache HIT/STALE results, including client navigation data.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, node.js, typescript
Domain
backend, performance
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.