[PR #1068] [MERGED] perf(pages): cache ISR misses from streamed render #1065

Closed
opened 2026-05-06 13:11:48 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/1068
Author: @NathanDrake2406
Created: 5/5/2026
Status: Merged
Merged: 5/5/2026
Merged by: @james-elicx

Base: mainHead: nathan/pages-isr-single-render


📝 Commits (3)

  • 4bfa8af perf(pages): cache ISR misses from streamed render
  • 09cf2fd fix(pages): keep ISR cache error reporting non-fatal
  • 46094f4 chore: rerun CI

📊 Changes

4 files changed (+196 additions, -40 deletions)

View changed files

📝 packages/vinext/src/entries/pages-server-entry.ts (+0 -1)
📝 packages/vinext/src/server/pages-page-response.ts (+109 -33)
📝 tests/pages-page-response.test.ts (+86 -5)
📝 tests/pages-router.test.ts (+1 -1)

📄 Description

What this changes

Pages Router ISR cache misses now cache the same body stream used for the live HTML response instead of rendering the page a second time just to build cache HTML.

The response path keeps the existing render-before-head-collection order, tees the completed body stream when ISR is eligible, sends one branch to the client, and records the other branch into the Pages ISR cache. Nonce-bearing responses still skip ISR writes.

Why

renderPagesPageResponse previously rendered the page once for the live response, then created a second page element and called renderIsrPassToStringAsync() to generate cached HTML. That second pass can repeat userland render work, data reads, Suspense work, and render-scoped state mutations on every Pages ISR MISS.

Next.js Pages ISR stores the generated RenderResult from the response cache path rather than constructing a second React tree for the cache fill. Vinext can do better for Workers by preserving streaming to the client while recording the same streamed body for persistence.

Approach

  • Keep the page render before shell/head collection so next/head, styled-jsx, and server-inserted HTML state are populated before shell assembly.
  • For cacheable nonce-free Pages ISR responses, tee the body stream after shell assembly.
  • Send branch A through the existing shell/body/shell composite response stream.
  • Drain branch B with streaming UTF-8 decoding and write shellPrefix + body + shellSuffix to ISR cache.
  • Report cache recording or cache write failures through the Pages Router render error path and avoid writing partial HTML.
  • Remove the now-unused renderIsrPassToStringAsync dependency from the Pages response helper call.

Validation

  • vp check packages/vinext/src/server/pages-page-response.ts packages/vinext/src/entries/pages-server-entry.ts tests/pages-page-response.test.ts tests/pages-router.test.ts
  • vp test run tests/pages-page-response.test.ts tests/pages-page-data.test.ts
  • vp test run tests/pages-router.test.ts -t "caches the streamed ISR render|Pages Router production fixture"
  • vp test run tests/features.test.ts -t "ISR \\(Pages Router\\)"
  • vp run vinext#build

The package build exits 0 and still emits the existing unresolved virtual-module warnings for virtual RSC/instrumentation imports.

Risks / follow-ups

This follows the existing App Router pattern of scheduling cache writes through ExecutionContext.waitUntil() when available. An immediate second request after a MISS can only hit once the cache backend has completed the write, which matches the asynchronous cache-write direction already used for App Router HTML caching.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/1068 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 5/5/2026 **Status:** ✅ Merged **Merged:** 5/5/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `nathan/pages-isr-single-render` --- ### 📝 Commits (3) - [`4bfa8af`](https://github.com/cloudflare/vinext/commit/4bfa8af0d6d83c8afdc8d6d5f62e013cb673fc9b) perf(pages): cache ISR misses from streamed render - [`09cf2fd`](https://github.com/cloudflare/vinext/commit/09cf2fdcdb69d9ca2dbbe90e7927067a1fd11325) fix(pages): keep ISR cache error reporting non-fatal - [`46094f4`](https://github.com/cloudflare/vinext/commit/46094f472cb8202280765f6d4c805618251b9357) chore: rerun CI ### 📊 Changes **4 files changed** (+196 additions, -40 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/entries/pages-server-entry.ts` (+0 -1) 📝 `packages/vinext/src/server/pages-page-response.ts` (+109 -33) 📝 `tests/pages-page-response.test.ts` (+86 -5) 📝 `tests/pages-router.test.ts` (+1 -1) </details> ### 📄 Description ## What this changes Pages Router ISR cache misses now cache the same body stream used for the live HTML response instead of rendering the page a second time just to build cache HTML. The response path keeps the existing render-before-head-collection order, tees the completed body stream when ISR is eligible, sends one branch to the client, and records the other branch into the Pages ISR cache. Nonce-bearing responses still skip ISR writes. ## Why `renderPagesPageResponse` previously rendered the page once for the live response, then created a second page element and called `renderIsrPassToStringAsync()` to generate cached HTML. That second pass can repeat userland render work, data reads, Suspense work, and render-scoped state mutations on every Pages ISR MISS. Next.js Pages ISR stores the generated `RenderResult` from the response cache path rather than constructing a second React tree for the cache fill. Vinext can do better for Workers by preserving streaming to the client while recording the same streamed body for persistence. ## Approach - Keep the page render before shell/head collection so `next/head`, styled-jsx, and server-inserted HTML state are populated before shell assembly. - For cacheable nonce-free Pages ISR responses, tee the body stream after shell assembly. - Send branch A through the existing shell/body/shell composite response stream. - Drain branch B with streaming UTF-8 decoding and write `shellPrefix + body + shellSuffix` to ISR cache. - Report cache recording or cache write failures through the Pages Router render error path and avoid writing partial HTML. - Remove the now-unused `renderIsrPassToStringAsync` dependency from the Pages response helper call. ## Validation - `vp check packages/vinext/src/server/pages-page-response.ts packages/vinext/src/entries/pages-server-entry.ts tests/pages-page-response.test.ts tests/pages-router.test.ts` - `vp test run tests/pages-page-response.test.ts tests/pages-page-data.test.ts` - `vp test run tests/pages-router.test.ts -t "caches the streamed ISR render|Pages Router production fixture"` - `vp test run tests/features.test.ts -t "ISR \\(Pages Router\\)"` - `vp run vinext#build` The package build exits 0 and still emits the existing unresolved virtual-module warnings for virtual RSC/instrumentation imports. ## Risks / follow-ups This follows the existing App Router pattern of scheduling cache writes through `ExecutionContext.waitUntil()` when available. An immediate second request after a MISS can only hit once the cache backend has completed the write, which matches the asynchronous cache-write direction already used for App Router HTML caching. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:11:48 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#1065
No description provided.