Upstream change
Next.js fixed a regression where pages rendered with draftMode().enable() would block on all I/O instead of streaming Suspense fallbacks. Since Next.js does not generate PPR shells for draft-mode requests, the renderer treats them as a static prerender and waits for the entire tree, even though the response is dynamic and could be streamed.
The fix adds isDraftMode to the conditions that opt the request into dynamic HTML rendering (alongside dev mode and non-SSG routes), so Suspense boundaries flush their fallbacks and the page streams normally.
Relevant diff (packages/next/src/build/templates/app-page.ts):
routeModule.isDev === true ||
// If this is a draft mode request, it supports dynamic HTML.
isDraftMode ||
// If this is not SSG or does not have static paths, then it supports
// dynamic HTML.
!isSSG ||
Why this matters for vinext
vinext renders App Router pages through its own RSC/SSR pipeline (entries/app-rsc-entry.ts and server/app-*.ts helpers). When draft mode is enabled we should make sure draft-mode requests are treated as dynamic and that Suspense fallbacks stream, rather than blocking on allReady like a static prerender. This is a parity item for cache components / "use cache" and any route that mixes draft mode with Suspense.
Suggested work
- Audit the App Router render path (
server/app-*.ts, RSC and SSR entries) to confirm draft-mode requests opt into the dynamic / streaming branch and do not wait for onAllReady before flushing.
- Port the fixture and test from
test/e2e/app-dir/cache-components/cache-components.draft-mode.test.ts (added in the PR) so we have explicit coverage that draft-mode pages stream their loading.tsx / Suspense fallbacks.
- Verify behavior on both the dev server and the Cloudflare Workers production entry.
Upstream change
Next.js fixed a regression where pages rendered with
draftMode().enable()would block on all I/O instead of streaming Suspense fallbacks. Since Next.js does not generate PPR shells for draft-mode requests, the renderer treats them as a static prerender and waits for the entire tree, even though the response is dynamic and could be streamed.The fix adds
isDraftModeto the conditions that opt the request into dynamic HTML rendering (alongside dev mode and non-SSG routes), so Suspense boundaries flush their fallbacks and the page streams normally.Relevant diff (
packages/next/src/build/templates/app-page.ts):Why this matters for vinext
vinext renders App Router pages through its own RSC/SSR pipeline (
entries/app-rsc-entry.tsandserver/app-*.tshelpers). When draft mode is enabled we should make sure draft-mode requests are treated as dynamic and that Suspense fallbacks stream, rather than blocking onallReadylike a static prerender. This is a parity item for cache components /"use cache"and any route that mixes draft mode with Suspense.Suggested work
server/app-*.ts, RSC and SSR entries) to confirm draft-mode requests opt into the dynamic / streaming branch and do not wait foronAllReadybefore flushing.test/e2e/app-dir/cache-components/cache-components.draft-mode.test.ts(added in the PR) so we have explicit coverage that draft-mode pages stream theirloading.tsx/ Suspense fallbacks.