-
Notifications
You must be signed in to change notification settings - Fork 30.2k
perf(cli): single-process dev server with Rust CLI wrapper #87941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: perf-dev-bytecode-cache
Are you sure you want to change the base?
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
5aa417d to
991c442
Compare
80e6a7b to
3b77b31
Compare
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
1 similar comment
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
991c442 to
ff4ff03
Compare
96f6373 to
f196646
Compare
5541176 to
e359b2b
Compare
f196646 to
58e04f1
Compare
58e04f1 to
7afaf27
Compare
e359b2b to
88f6518
Compare
CodSpeed Performance ReportMerging #87941 will not alter performanceComparing Summary
Footnotes
|
| await startServer({ | ||
| ...devServerOptions, | ||
| selfSignedCertificate: certificate, | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dev server no longer supports auto-restart when configuration files change if the Rust CLI wrapper is not available. When startServer() detects a config change, it calls process.exit(77), expecting a Rust wrapper to catch this exit code and restart the process. However, when falling back to the Node.js entry point, this exit code is not handled, causing the dev server to exit permanently instead of restarting.
View Details
📝 Patch Details
diff --git a/packages/next/bin/next b/packages/next/bin/next
index 84eee1314d..816b148735 100755
--- a/packages/next/bin/next
+++ b/packages/next/bin/next
@@ -10,4 +10,7 @@ for pkg in "$DIR"/../node_modules/@next/cli-*/next; do
done
# Fallback: Node.js
+# Set NEXT_CLI_NO_RUST to indicate we're running without the Rust wrapper.
+# This allows the Node.js dev server to implement its own restart loop for config changes.
+export NEXT_CLI_NO_RUST=1
exec node "$DIR/../dist/bin/next" "$@"
diff --git a/packages/next/src/cli/next-dev.ts b/packages/next/src/cli/next-dev.ts
index 16eadfa57c..49838014b7 100644
--- a/packages/next/src/cli/next-dev.ts
+++ b/packages/next/src/cli/next-dev.ts
@@ -228,10 +228,36 @@ const nextDev = async (
process.env.TURBOPACK = '1'
}
+ // Track if we're in a restart loop to handle exit code 77
+ // Only needed when running without the Rust CLI wrapper (NEXT_CLI_NO_RUST)
+ let restartExitCode: number | null = null
+ const originalExit = process.exit
+ const noRustWrapper = process.env.NEXT_CLI_NO_RUST === '1'
+
+ // Intercept process.exit to handle restart signals from the dev server
+ // When startServer() detects a config file change, it calls process.exit(77)
+ // The Rust wrapper catches this and restarts, but when running directly with Node.js,
+ // we need to handle it here to maintain dev server functionality
+ if (noRustWrapper) {
+ process.exit = function (code?: number | string) {
+ const exitCode = typeof code === 'string' ? parseInt(code, 10) : code || 0
+ // Exit code 77 signals "restart needed" (config file changed)
+ if (exitCode === 77) {
+ restartExitCode = 77
+ // Don't actually exit - we'll loop back and restart
+ return undefined
+ }
+ // For all other exit codes, actually exit the process
+ return originalExit.call(process, code as never)
+ } as any
+ }
+
const runDevServer = async (reboot: boolean) => {
try {
// Load startServer from bundled version with optional bytecode caching
- // The Rust wrapper handles restarts via exit code 77
+ // The Rust wrapper handles restarts via exit code 77.
+ // When running without the Rust wrapper, the intercepted process.exit() above
+ // will catch exit code 77 and trigger a restart.
const { startServer } =
require('../server/lib/start-server-with-cache') as typeof import('../server/lib/start-server-with-cache')
@@ -272,7 +298,29 @@ const nextDev = async (
}
}
- await runDevServer(false)
+ // Restart loop for the Node.js fallback path
+ // When NEXT_CLI_NO_RUST=1 (running without the Rust CLI wrapper),
+ // we need to handle config file changes and restart the server.
+ // The Rust wrapper has its own restart loop and won't reach this code.
+ if (noRustWrapper) {
+ let isRestart = false
+ while (true) {
+ restartExitCode = null
+ await runDevServer(isRestart)
+
+ // If startServer exited with code 77, loop back and restart
+ if (restartExitCode === 77) {
+ isRestart = true
+ continue
+ }
+ // Otherwise, exit normally
+ break
+ }
+ } else {
+ // When running with the Rust CLI wrapper, it handles the restart loop.
+ // We just run the server once and exit (potentially with code 77 for restarts).
+ await runDevServer(false)
+ }
}
export { nextDev }
Analysis
Dev server exits permanently when configuration files change in Node.js fallback mode
What fails: When running next dev without the Rust CLI wrapper (@next/cli-* binary), the dev server exits with code 77 and does not restart when configuration files change (next.config.js, next.config.mjs, tsconfig.json, etc.)
How to reproduce:
-
Run Next.js dev server directly through the Node.js fallback path (bypassing the Rust wrapper):
# Simulate the fallback: directly run Node.js instead of using the shell wrapper node packages/next/dist/bin/next dev -
Modify a config file that triggers the watcher (e.g., next.config.js, tsconfig.json)
-
Observe: The dev server logs "Found a change in [filename]. Restarting the server to apply the changes..." then exits with code 77
-
The dev server does not restart - it remains stopped
Expected behavior: The dev server should detect the config file change and restart the server, just like it does when using the Rust wrapper
Root cause: The architecture change in commit 88f6518 moved restart loop logic from JavaScript to a Rust wrapper. The Rust binary correctly handles exit code 77 by looping and restarting the Node.js process. However, when the Rust binary is unavailable and the shell script (packages/next/bin/next) falls back to directly executing node, there is no restart loop in the Node.js code to handle the exit code 77 signal from the config file watcher in start-server.ts.
Affected scenarios:
- Developers on unsupported platforms where Rust binaries aren't available
- Direct invocation of Node.js entry point (bypassing the shell script)
- Failed or missing binary installation (fallback to Node.js)
- Development environments where the Rust wrapper is not available
References:
- Entry point:
packages/next/bin/next(shell script) - Node.js entry point:
packages/next/src/bin/next.ts - Dev CLI:
packages/next/src/cli/next-dev.ts - Config file watcher:
packages/next/src/server/lib/start-server.ts(line 518) - Exit code constant:
packages/next/src/server/lib/utils.ts(RESTART_EXIT_CODE = 77) - Rust wrapper:
crates/next-cli/src/main.rs(handles restart loop)
88f6518 to
ef33eaa
Compare
7afaf27 to
39cecfc
Compare
5b4ce7a to
9bc50a8
Compare
39cecfc to
2258256
Compare
f07adf3 to
53ff208
Compare
8c03255 to
b0f627c
Compare
53ff208 to
8373a0d
Compare
b0f627c to
e226144
Compare
8373a0d to
c71c25a
Compare
c71c25a to
90e465a
Compare
e226144 to
35e7ae3
Compare
90e465a to
4687c5e
Compare
40bb559 to
64e7286
Compare
5b1b9fe to
74117b7
Compare
64e7286 to
3a74359
Compare
74117b7 to
b7ea57d
Compare
43a73eb to
c928af3
Compare
b7ea57d to
76d4345
Compare
c928af3 to
f3bca0e
Compare
76d4345 to
1b2bfdc
Compare
f3bca0e to
6d6e8c4
Compare
0bcb28e to
4893189
Compare
6d6e8c4 to
22510f8
Compare
Replace fork-based architecture with single Node.js process: - Add Rust CLI wrapper (`crates/next-cli/`) that handles: - NODE_OPTIONS (memory limits, source maps, inspect) - WATCHPACK_WATCHER_LIMIT for large projects - Restart loop (exit code 77) without Node.js overhead - Simplify next-dev.ts: remove fork(), IPC, child process management - Simplify start-server.ts: remove NEXT_PRIVATE_WORKER handling - Add shell entry points (bin/next, bin/next.cmd) that: - Use native binary when available (~10ms overhead) - Fall back to Node.js entry point - Add lazy loading for heavy modules: - tar (download-swc) - only needed for fallback download - edge-runtime sandbox - only needed for edge functions - MCP SDK - only needed when clients connect - Set up cross-platform release pipeline: - Build @next/cli-* for 8 platforms in CI - Publish as optional dependencies - Update install-native.mjs for development Saves ~35-50ms on dev server startup by eliminating parent Node.js process and fork overhead. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Yarn Berry PnP doesn't support shell script binaries - it tries to parse them as JavaScript. Convert bin/next from shell script to JavaScript with #!/usr/bin/env node shebang. The JS wrapper: - Detects platform/arch - Looks for Rust CLI binary in @next/cli-* packages - Falls back to dist/bin/next if not found Benchmark: ~25-35ms overhead vs shell script (negligible for 500-1500ms startup) Also removes next.cmd as npm/yarn auto-generate .cmd wrappers for JS bins.
4893189 to
b556af7
Compare
22510f8 to
f02153b
Compare
❌ 119 failing tests in 27 jobsUpdated 2026-01-05 10:01:41 UTC · Commit: f02153b Details📁
|

Summary
Replace fork-based architecture with single Node.js process:
Add Rust CLI wrapper (
crates/next-cli/) that handles:Simplify next-dev.ts: remove fork(), IPC, child process management
Simplify start-server.ts: remove NEXT_PRIVATE_WORKER handling
Add shell entry points (bin/next, bin/next.cmd) that:
Cross-platform release pipeline for 8 platforms
Test Fixes
The Rust CLI introduces exit code 77 for server restarts (e.g., after config file changes). Tests run Node.js directly without the Rust wrapper, so:
test/lib/next-modes/next-dev.ts- AddRESTART_EXIT_CODE = 77handling inNextDevInstance.start()to auto-restart the server when it exits with code 77, mimicking the Rust wrapper behaviorBenchmark Results (cumulative)
Biggest impact of all optimizations - eliminates Node.js fork overhead.
Test Plan