This moves the dev-mode Cache Components validation renders off the dev
server's main thread onto a worker thread, so rapid navigation no longer
starves the event loop. The worker crosses into the app-page bundle
exactly once, calling a new
`ComponentMod.routeModule.runValidationInDev` entry that rebuilds the
render context, work store, and request store from a serializable
snapshot and runs the whole validation there, so the client prerender
and the user's client components resolve the single app-page React
instance rather than a second copy.
A thin worker shell (`dev-validation-worker.ts`) plus a single-worker
pool (`dev-validation-worker-pool.ts`) load the user bundle via
`loadComponents`, install code-frame support for CLI output, and forward
the validation errors back as Flight bytes; the main thread only
delivers them to the overlay through `sendErrorsToBrowser`. The
snapshot, the globalThis-symbol handoff, and the shared error-delivery
helpers live in their own modules. The dev server installs the worker
when `experimental.devValidationWorker` is not `false`, and
`runDevValidationInBackground` uses it when present, falling back to the
in-process path otherwise. A one-slot `SharedArrayBuffer` propagates a
supersede abort into the worker so a newer navigation cancels an
in-flight validation.
The runtime bundle gains an `app-worker` entry for the worker with the
`build/swc` boundary externalized for the code-frame native binding, and
`patch-error-inspect.ts` now backs its code-frame renderer with a
globalThis symbol so all copies of the module share it across the
thread.
The synthetic `bench/dev-validation` benchmark navigates back-to-back,
so every click lands inside the validation window — the worst case for
main-thread contention. Browser-observed navigation TTFB in that window,
worker vs in-process:
| Route | Worker (p50/p95/max) | In-process (p50/p95/max) |
| ------ | -------------------- | ------------------------ |
| client | 19 / 24 / 27 ms | 40 / 66 / 7762 ms |
| server | 42 / 45 / 46 ms | 122 / 158 / 252 ms |
| sprite | 109 / 117 / 169 ms | 196 / 208 / 299 ms |
The steady-state difference is only tens of milliseconds; the effect
that matters is the tail. In-process, a navigation that collides with an
in-flight validation render can stall for seconds (this run peaked at
~7.8s on the client route, and the peak varies run to run) because a
staged render does not yield until it finishes; off-thread the main
thread stays free and that stall disappears.
Read these as an upper bound, not a speedup that generalizes. The work
moved off-thread is the validation render's CPU — bounded,
route-dependent, and free of IO — so it does not grow with the main
render's cost. In an app whose main render is dominated by IO the same
absolute saving is a small fraction of the request, and it only appears
when a navigation lands in the brief validation window; at ordinary
click speed it is largely invisible. This is a dev-only responsiveness
improvement whose benefit varies widely with the app and the navigation
pattern.
As a follow-up, the validation work that still runs on the main thread
could move to the worker as well. When the main render can't be reused
for validation (for example after a cache miss), the validation
re-renders on the main thread to produce its inputs, resuming from the
Resume Data Cache (RDC) the main render already filled, and only then
hands the resulting Flight chunks to the worker. A later iteration could
run those renders inside the worker too, transporting the (serializable)
RDC so the worker can resume from the filled caches rather than reading
them back on the main thread.
closes NAR-895
Works around a compiler bug in `SwcJsMinimizerRspackPlugin` that we
found in #95394
Some conditionals were getting miscompiled, from this:
```ts
if (inputsFromNavigation) {
if (areStagesCompatible) {
const instantInputs = needsInstantValidation ? inputsFromNavigation : null
const staticInputs = inputsFromNavigation
return { instantInputs, staticInputs }
}
if (navigationHasAppShell(navigationKind)) {
const instantInputs = needsInstantValidation ? inputsFromNavigation : null
const staticInputs = LAZY_RUNTIME_PRERENDER
return { instantInputs, staticInputs }
}
const instantInputs = needsInstantValidation ? LAZY_FULL_RENDER : null
const staticInputs = inputsFromNavigation
return { instantInputs, staticInputs }
}
```
into this (in
`packages/next/dist/compiled/next-server/app-page-turbo.runtime.dev.js`)
```js
if (inputsFromNavigation)
return areStagesCompatible || navigationHasAppShell(navigationKind)
? {
instantInputs: needsInstantValidation
? inputsFromNavigation
: null,
staticInputs: inputsFromNavigation,
}
: {
instantInputs: needsInstantValidation ? LAZY_FULL_RENDER : null,
staticInputs: inputsFromNavigation,
}
```
In the compiled version, the three branches get incorrectly merged into
two: if `navigationHasAppShell(navigationKind)`, static inputs are
`inputsFromNavigation` instead of `LAZY_RUNTIME_PRERENDER`. We're
guessing that this is a bug in some optimization of conditional returns;
disabling `compress.conditionals` in the minimizer seems to work around
it.
## Summary
- upgrade `@rspack/core` to `2.0.0-rc.0`
- sync the in-tree next rspack Rust binding/toolchain with newer rspack
releases
- align Next server runtime selection so `NEXT_RSPACK` uses the
turbo-like app runtime path
- include missing compiled `@next/react-refresh-utils` output in
`ncc-compiled`
## What is done
- `@next/rspack` Rust binding builds locally
- `packages/next` builds locally
- rspack-related tests are running and have been reduced to deeper
runtime compatibility failures
## Rspack test investigation
Latest checked CI run: `build-and-test` run `27129243230` at head
`484e7bf1b30655a9f94d0855b9e00774b4b38cd0`.
Confirmed Rspack / webpack parity gaps:
-
`test/e2e/edge-runtime-configurable-guards/edge-runtime-configurable-guards.test.ts`:
Rspack does not expose the webpack parser hooks used by
`MiddlewarePlugin`. The current Rspack path falls back to SWC analysis
in `finishModules`, which can report diagnostics but cannot apply the
parser-time transforms that wrap `eval` / `new Function`, track
`InnerGraph` usage for used vs unused dynamic code, or apply
`unstable_allowDynamic` with webpack parity. Dev therefore misses the
expected dynamic-code runtime warnings and also reports false
`process.cwd` Edge warnings from Next internals; prod fails
allowed/unused dynamic-code cases.
-
`test/development/middleware-overrides-node.js-api/middleware-overrides-node.js-api.test.ts`:
same parser-hook limitation. Webpack can observe that `process.cwd` is
overwritten before use, while the Rspack SWC fallback statically flags
`process.cwd` as an unsupported Edge Node API.
- `test/production/css-features/css-compilation.test.ts`: the CSS is
minified and prefixed as expected, but the Rspack lightningcss path
currently does not expose/control source map generation the way this
test expects, so emitted CSS has no `sourceMappingURL` comment or `.map`
file to assert.
- `test/e2e/app-dir/server-source-maps/server-source-maps.test.ts`:
invalid source maps are not handled with the same graceful diagnostic as
webpack/Node source-map handling. Rspack logs the raw `webpack-internal`
frame for the damaged map case instead of surfacing the expected
`Invalid source map. Only conformant source maps...` message with the
cause.
-
`test/e2e/app-dir/parallel-routes-revalidation/parallel-routes-revalidation.test.ts`:
raw CI output observed a dev-mode timeout waiting for `networkidle`
after refresh/back/forward lazy fetching. This was not the final
structured failure in the latest run, but it remains a Rspack dev parity
follow-up around router cache lazy fetches and network settling.
- `test/e2e/middleware-src/middleware-src.test.ts`: after the test adds
root middleware files, Rspack dev still lets `src/middleware`
participate; the expected behavior is that only the root middleware
runs.
- `test/e2e/env-config/env-config.test.ts`: changing `.env` logs `Reload
env:` but the client keeps the previous `NEXT_PUBLIC_` value, pointing
at an Rspack dev env invalidation/HMR gap.
-
`test/e2e/app-dir/use-cache-without-experimental-flag/use-cache-without-experimental-flag.test.ts`:
after enabling the `useCache` flag, the dev server does not finish
restarting, pointing at build-error recovery/restart parity.
-
`test/development/app-dir/server-navigation-error/server-navigation-error.test.ts`:
middleware navigation-error cases hit `ERR_CONNECTION_REFUSED`,
consistent with the Rspack dev server exiting or restarting unexpectedly
during those error flows.
- `test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts`: the
custom-server dev process exits before the SDK handle is initialized,
then cleanup fails on `shutdown`; this needs follow-up in Rspack
dev/custom-server startup.
- `test/e2e/next-image-new/app-dir/app-dir.test.ts`: after toggling
image source, `onLoadingComplete` still reports the previous image
dimensions/source, pointing at an Rspack dev image/HMR or asset
invalidation parity issue.
- `test/e2e/node-cli-args/node-cli-args.test.ts`: on Node `20.19.x`,
`node --experimental-network-inspection` resolves instead of rejecting.
This looks like a Node-version behavior change rather than an Rspack
compiler issue.
Not attributed to this Rspack upgrade:
-
`test/e2e/app-dir/instant-navigation-testing-api/instant-navigation-testing-api.test.ts`
was marked by `scripts/pr-status.js` as a known flaky test across
branches.
-
`test/production/app-dir/build-output-prerender/build-output-prerender.test.ts`
ran in the webpack prod job and only differs by prerender error ordering
in the inline snapshot.
<!-- NEXT_JS_LLM_PR -->
---------
Co-authored-by: Benjamin Woodruff <benjamin.woodruff@vercel.com>
### What?
Remove `experimental.useNodeStreams` from the public config surface and
make Node streams always-on for Node.js App Router rendering.
The smaller default-on change and dedicated CI cleanup landed in #94311.
The standalone compatibility update landed separately in #94347.
### Why?
Node streams are now the default Node.js runtime rendering path, so the
opt-in config and runtime selection plumbing are stale. Edge bundles
continue to use the web-stream path because Node streams are unavailable
there.
### How?
- Define `__NEXT_USE_NODE_STREAMS` as true for Node.js app runtime
bundles and false for edge user bundles.
- Remove config, schema, runtime, export, and server plumbing for
`experimental.useNodeStreams`.
- Delete the obsolete env-precedence fixture.
- Clean benchmark docs and scripts that still set or compare
`experimental.useNodeStreams`.
### Verification
- `pnpm --filter=next types`
- `pnpm build-all`
<!-- NEXT_JS_LLM_PR -->
Follow-up to #93500.
The probe worker (`use-cache-probe-worker.ts`) ships as plain `tsc`
output and `require()`s
`react-server-dom-webpack/server` directly. The merged PR added a
require-hook entry covering only that one specifier, which is not
enough. The vendored RSC binding itself does plain `require('react')`
and `require('react-dom')`, and with `--conditions=react-server` set on
the worker those fall through to the user's installed React. For any
Next.js app on React 18 that means React 18's `react.shared-subset`, a
stub that throws `"not yet supported outside of experimental channels"`.
The probe crashes the moment it spawns. Default canary tests didn't
catch this — their installed React 19 supports the `react-server`
condition — but running the suite with
`NEXT_TEST_REACT_VERSION="18.3.1"` surfaced it.
Adding more hook entries to cover `react`, `react-dom`, etc. would mean
manually mirroring the `${bundler} × ${reactChannel}` matrix that
`makeAppAliases` and the `react-server` layer rule in
`next-runtime.webpack-config.js` already encode for `react`,
`react-dom`, `react/jsx-*`, and
`react-server-dom-{webpack,turbopack}/*`.
The worker is now bundled through that pipeline as a new `'app-worker'`
bundleType, which inherits the alias matrix and is included in the
`react-server.node` layer rule. Four dev-only tasks (`{turbo,webpack} ×
{experimental,stable}`) produce the four artifacts. In
`use-cache-probe-pool.ts` when the pool is created we resolve the
matching bundle via `process.env.TURBOPACK` and
`needsExperimentalReact(nextConfig)`.
The worker source uses a plain static import of
`react-server-dom-webpack/server`, as we do everywhere else. The
require-hook entry is deleted, and `--conditions=react-server` is no
longer added to `NODE_OPTIONS` since all React specifiers resolve to
absolute paths at build time.
When running `next build --debug-prerender`, React owner stacks are now
captured and displayed in prerender error output. This makes it much
easier to diagnose which component triggered uncached I/O or accessed
request data without Suspense. Previously, `--debug-prerender` only
enabled source maps and disabled minification. Now it also auto-enables
`allowDevelopmentBuild` and sets `NODE_ENV=development`, which loads
React development builds where `captureOwnerStack()` is available.
The main challenge is that with `NODE_ENV=development`, both server and
client bundles include dev-only code paths (HMR, WebSocket connections,
dev overlay, debug channel, etc.) that expect a running dev server. We
don't want these when using `next start`. To solve this, we introduce
`process.env.__NEXT_DEV_SERVER`, an internal env var that is truthy only
during `next dev`. In client bundles, it's inlined at build time (`'1'`
for `next dev`, `''` for `next build`). In production server runtime
bundles, it's inlined as `''` for dead-code elimination. In development
server runtime bundles, it's left as a runtime check because those
bundles are shared between `next dev` (where it's set) and `next build
--debug-prerender` (where it's not). Meanwhile, `NODE_ENV` continues to
control React's dev/prod mode and error formatting, which is exactly
what we want for `--debug-prerender`.
This also replaces the previous `renderOpts.dev` / `workStore.dev`
pattern, which was unreliable because `RouteModule.isDev` was derived
from `NODE_ENV` at compile time. When `allowDevelopmentBuild` set
`NODE_ENV=development`, `isDev` would be compiled as `true` and
incorrectly activate all dev guards during `next start`.
Key changes:
- `config.ts` auto-enables `allowDevelopmentBuild` and sets
`NODE_ENV=development` when `--debug-prerender` is active
- `define-env.ts` inlines `__NEXT_DEV_SERVER` into all bundles (truthy
for dev, falsy for build) so dev-server features are dead-code
eliminated in production and `--debug-prerender` builds
- `next-dev.ts` and `next.ts` set `__NEXT_DEV_SERVER` in the process
environment for externalized server-side code
- `renderOpts.dev` and `workStore.dev` are removed — all consumers now
use `__NEXT_DEV_SERVER` (for dev-server features) or `NODE_ENV` (for
error formatting that should work in both dev and `--debug-prerender`
builds)
- `patch-error-inspect.ts` devirtualizes React server URLs in source map
URLs so they display as readable file paths
## What?
This PR introduces the Resume Data Cache (RDC) functionality to improve
handling of dynamic React Server Component (RSC) requests when Partial
Pre-rendering (PPR) is enabled. This enhancement enables dynamic RSC
responses to utilize the Resume Data Cache, significantly reducing
waterfalls and preventing content shearing.
## Why?
Previously, dynamic RSC requests with PPR enabled experienced several
issues:
**Waterfall Reduction:** Dynamic RSC responses required multiple
waterfalls to hydrate a given cache, leading to slower response times
and unnecessary network round-trips.
**Content Shearing Prevention:** There were mismatches between static
cached parts of the page and the dynamic response, causing visual
inconsistencies and potential hydration errors. By ensuring the RDC
generated for static responses is also used for dynamic RSC responses,
we maintain consistency even when the dynamic response interacts with
caches directly and potentially receives newer, non-stale values.
**Additional Issues Addressed:**
- Stale content revalidation not triggering correctly
- Background revalidation producing dynamic responses instead of static
ones
- Inconsistent cache tag staleness evaluation
- Missing support for resumable rendering in dynamic contexts
## How?
**Core Implementation:**
- Added Resume Data Cache layer that stores resumable render data for
dynamic RSC requests
- Ensures the same RDC used by static responses is reused for dynamic
RSC responses
- Introduced `forceStaticRender` flag to ensure background revalidation
generates static responses
- Refactored response cache internals to separate cache entry creation
from response handling
- Enhanced error handling with try-catch blocks for server actions in
app-render
**Revalidation Improvements:**
- Fixed cache tag staleness evaluation for consistent behavior
- Improved triggering logic for background revalidation of stale dynamic
content
- Better handling of cache misses and "no-resume-data" scenarios with
debug logging
---------
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Closes https://linear.app/vercel/issue/NEXT-4560/
React now exposes Web stream APIs in their Node.js builds so we can use the Node.js builds.
That enable us to use some of the Node.js goodies like `async_hooks` which is required for the React's experimental Server Requests track.
## Test plan
`next-server` runtimes did not increase bundle size. We even saved some in the `app-page` entries since we no longer have to bundle Edge and Node.js versions of React Server. We can rely solely on the Node.js variant of React Server.
A build of `test/e2e/app-dir/hello-world` shows no significant bundle size changes
These externals have no effect since we don't import the legacy `react-dom/server` APIs in `next-server` anyway.
We only import the modern APIs since we alias `react-dom/server` to `build/webpack/alias/react-dom-server-edge.js` which does not import from the legacy entrypoint.
## Test plan
- [x] `compiled/next-server` remained unchanged
- Moved source files from `_experimental/` to `ui/`.
- Moved `dev-indicators` sources out of `internal/`.
- Renamed files and variables to have explicit `app-`, `App`, `pages-`
or `Pages` prefixes for better distinguish.
- Updated snapshots that were caused by the name change.
- Added better type to `next-runtime.webpack-config.js`.
- Moved `font/` out of `ui/`.
---------
Co-authored-by: Jiachi Liu <inbox@huozhi.im>
Co-authored-by: Jimmy Lai <laijimmy0@gmail.com>
This reverts the bundler change part while we investigate source map
issue but keeps the externals optimize preventing the runtime bundle
from inflating back to over `4MB`.
This reverts commit 98b0700e97
This reverts commit 04c3f0b29c
This improves the build script quite a bit and makes dev editing much
faster. Also fixed excludes causing webpack/babel compiled packages to
be bundled into the runtime un-necessarily reducing from `10MB` ->
`1.2MB`.
Before: `Finished next_bundle in 9.67s`
After: `Finished next_bundle in 1.27s`
Validated here
https://github.com/vercel/vercel/actions/runs/12957911052/job/36147343396?pr=12914