Skip to content

Build with dev runtimes when --debug-prerender is set#89834

Merged
unstubbable merged 18 commits intocanaryfrom
hl/build-time-owner-stacks
Feb 13, 2026
Merged

Build with dev runtimes when --debug-prerender is set#89834
unstubbable merged 18 commits intocanaryfrom
hl/build-time-owner-stacks

Conversation

@unstubbable
Copy link
Contributor

@unstubbable unstubbable commented Feb 11, 2026

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

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 11, 2026

Tests Passed

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 11, 2026

Stats from current PR

🟢 1 improvement

Metric Canary PR Change Trend
node_modules Size 472 MB 472 MB 🟢 103 kB (0%) █████
📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 456ms 456ms ▁▁▁▁▁
Cold (Ready in log) 441ms 442ms ▂▁▂▂▂
Cold (First Request) 835ms 833ms ▅▁▄▄▄
Warm (Listen) 455ms 456ms ▁▁▁▁▁
Warm (Ready in log) 438ms 439ms ▁▁▁▁▁
Warm (First Request) 338ms 338ms ▁▁▁▁▁
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 456ms 455ms ▁▁█▁▁
Cold (Ready in log) 444ms 443ms ▃▁█▅▂
Cold (First Request) 1.852s 1.866s ▂▁█▃▁
Warm (Listen) 456ms 456ms ▁▁█▁▁
Warm (Ready in log) 444ms 445ms ▃▁█▄▁
Warm (First Request) 1.888s 1.881s ▂▁█▃▁

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 4.100s 4.035s ▁▂▁▁▁
Cached Build 4.063s 4.042s ▁▂▁▁▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.046s 14.001s ▁▁█▂▁
Cached Build 14.028s 14.081s ▁▁█▂▁
node_modules Size 472 MB 472 MB 🟢 103 kB (0%) █████
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles: **437 kB** → **437 kB** ✅ -82 B

81 files with content-based hashes (individual files not comparable between builds)

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 761 B 768 B
Total 761 B 768 B ⚠️ +7 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 449 B 449 B
Total 449 B 449 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.47 kB N/A -
6280-HASH.js gzip 57 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.53 kB N/A -
e8aec2e4-HASH.js gzip 62.5 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 255 B 254 B
main-HASH.js gzip 39.1 kB 39.1 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.53 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.49 kB -
6948ada0-HASH.js gzip N/A 62.5 kB -
9544-HASH.js gzip N/A 57.7 kB -
Total 230 kB 231 kB ⚠️ +639 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.49 kB 2.49 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.97 kB 7.97 kB ✅ -1 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 126 kB 125 kB
page.js gzip 249 kB 249 kB
Total 375 kB 375 kB ✅ -764 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 615 B 615 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 33.1 kB 32.9 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 34.7 kB 34.5 kB ✅ -190 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 733 B 735 B
Total 733 B 735 B ⚠️ +2 B
Build Cache
Canary PR Change
0.pack gzip 3.84 MB 3.85 MB 🔴 +7.72 kB (+0%)
index.pack gzip 102 kB 104 kB 🔴 +1.56 kB (+2%)
index.pack.old gzip 104 kB 103 kB
Total 4.05 MB 4.05 MB ⚠️ +8.63 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 316 kB 316 kB
app-page-exp..prod.js gzip 168 kB 167 kB
app-page-tur...dev.js gzip 315 kB 315 kB
app-page-tur..prod.js gzip 167 kB 167 kB
app-page-tur...dev.js gzip 312 kB 312 kB
app-page-tur..prod.js gzip 166 kB 165 kB
app-page.run...dev.js gzip 312 kB 312 kB
app-page.run..prod.js gzip 166 kB 165 kB
app-route-ex...dev.js gzip 70.5 kB 70.5 kB
app-route-ex..prod.js gzip 49 kB 49 kB
app-route-tu...dev.js gzip 70.5 kB 70.5 kB
app-route-tu..prod.js gzip 49 kB 49 kB
app-route-tu...dev.js gzip 70.1 kB 70.1 kB
app-route-tu..prod.js gzip 48.8 kB 48.8 kB
app-route.ru...dev.js gzip 70.1 kB 70.1 kB
app-route.ru..prod.js gzip 48.8 kB 48.7 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.2 kB 43.2 kB
pages-api-tu..prod.js gzip 32.9 kB 32.9 kB
pages-api.ru...dev.js gzip 43.2 kB 43.2 kB
pages-api.ru..prod.js gzip 32.8 kB 32.8 kB
pages-turbo....dev.js gzip 52.5 kB 52.5 kB
pages-turbo...prod.js gzip 39.4 kB 38.4 kB 🟢 944 B (-2%)
pages.runtim...dev.js gzip 52.5 kB 52.5 kB
pages.runtim..prod.js gzip 39.4 kB 38.4 kB 🟢 946 B (-2%)
server.runti..prod.js gzip 62.7 kB 63.5 kB 🔴 +817 B (+1%)
Total 2.8 MB 2.79 MB ✅ -3.55 kB
📝 Changed Files (23 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-exp..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page.runtime.dev.js
  • app-page.runtime.prod.js
  • app-route-ex..ntime.dev.js
  • app-route-ex..time.prod.js
  • app-route-tu..ntime.dev.js
  • app-route-tu..time.prod.js
  • app-route-tu..ntime.dev.js
  • app-route-tu..time.prod.js
  • app-route.runtime.dev.js
  • app-route.ru..time.prod.js
  • pages-api-tu..ntime.dev.js
  • pages-api.runtime.dev.js
  • pages-turbo...ntime.dev.js
  • pages-turbo...time.prod.js
  • ... and 3 more
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js

Diff too large to display

app-route-ex..ntime.dev.js

Diff too large to display

app-route-ex..time.prod.js

Diff too large to display

app-route-tu..ntime.dev.js

Diff too large to display

app-route-tu..time.prod.js

Diff too large to display

app-route-tu..ntime.dev.js

Diff too large to display

app-route-tu..time.prod.js

Diff too large to display

app-route.runtime.dev.js

Diff too large to display

app-route.ru..time.prod.js

Diff too large to display

pages-api-tu..ntime.dev.js

Diff too large to display

pages-api.runtime.dev.js

Diff too large to display

pages-turbo...ntime.dev.js

Diff too large to display

pages-turbo...time.prod.js

Diff too large to display

pages.runtime.dev.js

Diff too large to display

pages.runtime.prod.js

Diff too large to display

server.runtime.prod.js

Diff too large to display

vercel[bot]

This comment was marked as outdated.

@unstubbable unstubbable force-pushed the hl/build-time-owner-stacks branch 3 times, most recently from 57e45d9 to 0e6d5ee Compare February 12, 2026 10:40
When running `next build --debug-prerender`, React owner stacks are now
captured and displayed in prerender error output. This makes it much
easier to trace which component triggered uncached I/O or accessed
request data without Suspense, matching the error quality you'd get from
`next dev`.

This works by auto-enabling `allowDevelopmentBuild` and setting
`NODE_ENV=development` when `--debug-prerender` is active, which
switches to React development builds where `captureOwnerStack()` is
available. Since this bakes `NODE_ENV=development` into the bundles at
build time, dev-only features like HMR, WebSocket connections, and debug
channels would incorrectly activate during `next start`. To prevent
this, all dev-server-specific code paths now check
`process.env.NEXT_PHASE` (which reflects the actual runtime phase)
instead of `NODE_ENV` or `renderOpts.dev`.

Key changes:

- `config.ts` auto-enables `allowDevelopmentBuild` and sets
  `NODE_ENV=development` when `debugPrerender` is active
- `define-env.ts` inlines `NEXT_PHASE` into client bundles so dev-server
  features (HMR, WebSocket, dev overlay, debug channels) are dead-code
  eliminated in `--debug-prerender` builds
- `router-server.ts` sets `process.env.NEXT_PHASE` at startup so
  server-side code can distinguish `next dev` from `next start`
- `route-module.ts` derives `isDev` from `NEXT_PHASE` instead of
  `NODE_ENV` to avoid baking the wrong value at build time
- `renderOpts.dev` and `workStore.dev` are removed — all consumers now
  use `NEXT_PHASE` (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 owner
  stacks so they display as readable file paths
- `static-paths/app.ts` uses `NEXT_PHASE` instead of `NODE_ENV` to
  determine build-time route pregeneration support
Avoids incompatibility with the Edge runtime, which doesn't support
`process.features` and causes the entire `constants` module to fail
loading when imported in that environment.
All callers either pass the result to `findSourceMap`/`nativeFindSourceMap` (needs encoded URL) or to display via `frameToString` which handles `file://` URLs through `url.fileURLToPath()` (decodes properly). Removing `decodeURI` should be safe for all callers.
@unstubbable unstubbable force-pushed the hl/build-time-owner-stacks branch from fb8b80a to daca900 Compare February 12, 2026 14:46
@unstubbable unstubbable force-pushed the hl/build-time-owner-stacks branch from 5aaa497 to da40d0f Compare February 12, 2026 16:03
So that relative `sources` in the source map resolve against the real
chunk URL, not the virtual one.
@unstubbable unstubbable marked this pull request as ready for review February 12, 2026 19:27
@vercel
Copy link
Contributor

vercel bot commented Feb 12, 2026

Notifying the following users due to files changed in this PR based on this repo's notify modifiers:

@timneutkens, @ijjk, @shuding, @huozhi:

packages/next/src/server/config.ts

@unstubbable unstubbable requested a review from eps1lon February 13, 2026 08:03
Copy link
Member

@eps1lon eps1lon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would adjust the PR title. This is way more invasive than just owner stacks. How does --debug-prerender interact with next start e.g. do we consider the built app runnable or do we not guarantee that it works?

@unstubbable
Copy link
Contributor Author

I would adjust the PR title. This is way more invasive than just owner stacks.

How about "Build with dev runtimes when --debug-prerender is set"?

How does --debug-prerender interact with next start e.g. do we consider the built app runnable or do we not guarantee that it works?

This generally works, I'm not sure about guarantees though. I'd still consider it an edge case, as next build --debug-prerender is mostly meant for CLI-focused debugging, not browser-focused debugging. For that, we still recommend using next dev.

We do have a couple of tests in cache-components-errors.test.ts that don't fail the build and use next start for some runtime assertions. They cover testing that next start generally works.

We're also using the dev bundles for the browser in that case. But no Next.js dev server features are enabled (e.g. React can't fetch source maps for server components via findSourceMapURL).

Screenshot 2026-02-13 at 10 14 50

@unstubbable unstubbable changed the title Enable owner stacks during next build --debug-prerender Build with dev runtimes when --debug-prerender is set Feb 13, 2026
@unstubbable unstubbable merged commit e9b96b4 into canary Feb 13, 2026
159 of 160 checks passed
@unstubbable unstubbable deleted the hl/build-time-owner-stacks branch February 13, 2026 15:29
unstubbable added a commit that referenced this pull request Feb 13, 2026
Follow-up for #89834.

In `prerenderAndAbortInSequentialTasks`, React's `finishHalt` (scheduled
via `setImmediate` from `abort()`) could race with the component's
pending `setTimeout` callback. When both ended up in the same timer
phase, the component timer would fire after `abort()` but before
`finishHalt`, linking the async graph and producing the more precise
`await` location (21:9). When `finishHalt` won the race, it read an
unlinked graph and fell back to the function declaration (20:16).

Adding `DANGEROUSLY_runPendingImmediatesAfterCurrentTask()` to the abort
task captures `finishHalt` as a fast immediate, ensuring it runs right
after `abort()` before any other timers. This makes the result
deterministic at the cost of always producing the less precise stack
frame (function declaration instead of `await` expression). The resolve
step is split into a separate task because it needs to wait for the
abort's fast immediates to complete.

When using `--debug-prerender`, we're also re-adding the hint to run
`next dev` for even better stack traces, since dev mode can produce the
precise `await` location by running the render to completion and
resolving the I/O promises.

[Flakiness metric](https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%22fail%22%20%40test.suite%3A%22Cache%20Components%20Errors%22%20%40git.branch%3Acanary%20-%40ci.pipeline.name%3Atest-e2e-deploy-release&agg_m=count&agg_m_source=base&agg_t=count&currentTab=overview&eventStack=&fromUser=false&index=citest&start=1770404554855&end=1771009354855&paused=false)
unstubbable added a commit that referenced this pull request Feb 13, 2026
…ces (#89969)

Follow-up for #89834.

In `prerenderAndAbortInSequentialTasks`, React's `finishHalt` (scheduled
via `setImmediate` from `abort()`) could race with the component's
pending `setTimeout` callback. When both ended up in the same timer
phase, the component timer would fire after `abort()` but before
`finishHalt`, linking the async graph and producing the more precise
`await` location (21:9). When `finishHalt` won the race, it read an
unlinked graph and fell back to the function declaration (20:16).

Adding `DANGEROUSLY_runPendingImmediatesAfterCurrentTask()` to the abort
task captures `finishHalt` as a fast immediate, ensuring it runs right
after `abort()` before any other timers. This makes the result
deterministic at the cost of always producing the less precise stack
frame (function declaration instead of `await` expression). The resolve
step is split into a separate task because it needs to wait for the
abort's fast immediates to complete.

When using `--debug-prerender`, we're also re-adding the hint to run
`next dev` for even better stack traces, since dev mode can produce the
precise `await` location by running the render to completion and
resolving the I/O promises.

[Flakiness
metric](https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%22fail%22%20%40test.suite%3A%22Cache%20Components%20Errors%22%20%40git.branch%3Acanary%20-%40ci.pipeline.name%3Atest-e2e-deploy-release&agg_m=count&agg_m_source=base&agg_t=count&currentTab=overview&eventStack=&fromUser=false&index=citest&start=1770404554855&end=1771009354855&paused=false)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants