[PR #404] perf: eliminate page probe double-execution of page components #548

Open
opened 2026-05-06 13:08:42 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/404
Author: @james-elicx
Created: 3/10/2026
Status: 🔄 Open

Base: mainHead: perf/eliminate-page-probe-double-execution


📝 Commits (1)

  • a51efe3 perf: eliminate page probe double-execution of page components

📊 Changes

2 files changed (+616 additions, -294 deletions)

View changed files

📝 packages/vinext/src/entries/app-rsc-entry.ts (+88 -42)
📝 tests/__snapshots__/entry-templates.test.ts.snap (+528 -252)

📄 Description

The RSC entry had a "page probe" that called PageComponent() outside React's render cycle before every RSC render, solely to detect redirect()/notFound()/forbidden()/unauthorized() throws. The probe's result was discarded — only thrown errors mattered. Then the actual RSC render called the same component again, executing all data fetching and computation a second time.

For async server components (the common case — pages that fetch data), this meant every fetch, database query, and API call ran twice per request. On Cloudflare Workers, where there's no persistent process cache to deduplicate fetches, this doubled CPU time for data-heavy pages.

Investigation of Next.js source confirmed they do NOT use a probe. They handle redirect/notFound entirely during rendering: errors inside Suspense boundaries are caught by React error boundaries (rendered in-stream with status 200), while errors that escape all boundaries are caught by an outer try/catch before any bytes are flushed.

Replace the page probe with three strategies based on page type:

  1. Routes WITH loading.tsx (Suspense): stream the RSC response directly. Errors inside the Suspense boundary flow through the RSC stream as error references for client-side handling, matching Next.js behavior.

  2. SYNC pages (no loading.tsx): a lightweight sync-only probe catches redirect/notFound thrown synchronously by the page function. This is practically free — sync server components don't do data fetching. Errors from async children inside inline boundaries flow through the stream as before.

  3. ASYNC pages (no loading.tsx): buffer the entire RSC stream, then check onError for redirect/notFound. Components execute only once during the actual RSC render. Latency characteristics are identical because without Suspense, nothing can stream until all async work completes anyway.

The layout probe is preserved unchanged — it's needed for correct parent-boundary selection when a layout throws notFound() (knowing which layout threw determines which not-found.tsx boundary to render).


🔄 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/404 **Author:** [@james-elicx](https://github.com/james-elicx) **Created:** 3/10/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `perf/eliminate-page-probe-double-execution` --- ### 📝 Commits (1) - [`a51efe3`](https://github.com/cloudflare/vinext/commit/a51efe3a2fee728689b01ace1e04e00e15b784a5) perf: eliminate page probe double-execution of page components ### 📊 Changes **2 files changed** (+616 additions, -294 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/entries/app-rsc-entry.ts` (+88 -42) 📝 `tests/__snapshots__/entry-templates.test.ts.snap` (+528 -252) </details> ### 📄 Description The RSC entry had a "page probe" that called PageComponent() outside React's render cycle before every RSC render, solely to detect redirect()/notFound()/forbidden()/unauthorized() throws. The probe's result was discarded — only thrown errors mattered. Then the actual RSC render called the same component again, executing all data fetching and computation a second time. For async server components (the common case — pages that fetch data), this meant every fetch, database query, and API call ran twice per request. On Cloudflare Workers, where there's no persistent process cache to deduplicate fetches, this doubled CPU time for data-heavy pages. Investigation of Next.js source confirmed they do NOT use a probe. They handle redirect/notFound entirely during rendering: errors inside Suspense boundaries are caught by React error boundaries (rendered in-stream with status 200), while errors that escape all boundaries are caught by an outer try/catch before any bytes are flushed. Replace the page probe with three strategies based on page type: 1. Routes WITH loading.tsx (Suspense): stream the RSC response directly. Errors inside the Suspense boundary flow through the RSC stream as error references for client-side handling, matching Next.js behavior. 2. SYNC pages (no loading.tsx): a lightweight sync-only probe catches redirect/notFound thrown synchronously by the page function. This is practically free — sync server components don't do data fetching. Errors from async children inside inline <Suspense> boundaries flow through the stream as before. 3. ASYNC pages (no loading.tsx): buffer the entire RSC stream, then check onError for redirect/notFound. Components execute only once during the actual RSC render. Latency characteristics are identical because without Suspense, nothing can stream until all async work completes anyway. The layout probe is preserved unchanged — it's needed for correct parent-boundary selection when a layout throws notFound() (knowing which layout threw determines which not-found.tsx boundary to render). --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
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#548
No description provided.