Description
Link to the code that reproduces this issue
https://github.com/Soarex16/next-15-server-components-reproduction-app
To Reproduce
- Start the application in dev mode (either
next dev
ornext dev --turbopack
) - Set a breakpoint on
app/page.tsx:3
(on theconsole.log
) - Start debugging in a browser
Current vs. Expected behavior
Expected behavior
The debugger will stop in app/page.tsx
only once (at the breakpoint on line 3).
After resuming the code, the debugger will not stop.
Actual behavior
The debugger stops at app/page.tsx:3
, but after resume it stops at random locations in the same file.
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 24.3.0: Thu Jan 2 20:24:23 PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6020
Available memory (MB): 65536
Available CPU cores: 12
Binaries:
Node: 20.9.0
npm: 10.1.0
Yarn: 1.22.21
pnpm: 8.10.5
Relevant Packages:
next: 15.3.0-canary.29 // Latest available version is detected (15.3.0-canary.29).
eslint-config-next: N/A
react: 19.1.0
react-dom: 19.1.0
typescript: 5.8.2
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
Not sure
Which stage(s) are affected? (Select all that apply)
next dev (local)
Additional context
TLDR
- React Server components machinery creates fake scripts with the same source map as a real user code to preserve source mapped traces in the browser.
This confuses js debuggers because they think that this piece of code was bundled in different places multiple times.
So when the user put a breakpoint, the debugger put breakpoints in all these scripts causing redundant breaks. - This issue affects server components debugging both Webpack and Turbopack
- This bug was introduced somewhere between 15.1.7 and 15.2. Sorry, I didn't have enough time to bisect it.
- Reproducible with all JS popular debuggers (Chrome DevTools, WebStorm, VS Code). What's interesting is, in Firefox it works as expected.
- Video with debugging in Chrome DevTools:
Screen.Recording.2025-03-31.at.16.34.39.1.mov
Detailed investigation
I discovered this issue working on turbopack support in WebStorm.
First I thought that is another bug in our JS debugger, but after checking CDP logs
I noticed that Next.js creates a lot of scripts with url like rsc://React/Server/webpack-internal:///(rsc)/./app/page.tsx?SOME_NUMBER
.
Here is an example of one of the scripts:
/* This module was rendered by a Server Component. Turn on Source Maps to see the server source. */
({"Home":_=>
_()})
//# sourceURL=rsc://React/Server/webpack-internal:///(rsc)/./app/page.tsx?0
//# sourceMappingURL=http://localhost:3000/__nextjs_source-map?filename=webpack-internal%3A%2F%2F%2F%28rsc%29%2F.%2Fapp%2Fpage.tsx
All these scripts created at react-server-dom-webpack-client.browser.development.js
.
Here is one of the creation traces of such fake scripts
page.tsx:5 <<< This stack frame was source mapped from the fake scripts
createFakeFunction(), react-server-dom-webpack-client.browser.development.js:1954
buildFakeCallStack(), react-server-dom-webpack-client.browser.development.js:1976
react-stack-bottom-frame(), react-server-dom-webpack-client.browser.development.js:2595
anonymous(), react-server-dom-webpack-client.browser.development.js:2331
initializeModelChunk(), react-server-dom-webpack-client.browser.development.js:1054
getOutlinedModel(), react-server-dom-webpack-client.browser.development.js:1327
parseModelString(), react-server-dom-webpack-client.browser.development.js:1540
anonymous(), react-server-dom-webpack-client.browser.development.js:2294
initializeModelChunk(), react-server-dom-webpack-client.browser.development.js:1054
resolveModelChunk(), react-server-dom-webpack-client.browser.development.js:1031
resolveModel(), react-server-dom-webpack-client.browser.development.js:1599
processFullStringRow(), react-server-dom-webpack-client.browser.development.js:2288
processFullBinaryRow(), react-server-dom-webpack-client.browser.development.js:2233
progress(), react-server-dom-webpack-client.browser.development.js:2479
Async call from Promise.then
progress(), react-server-dom-webpack-client.browser.development.js:2499
Async call from Promise.then
progress(), react-server-dom-webpack-client.browser.development.js:2499
Async call from Promise.then
progress(), react-server-dom-webpack-client.browser.development.js:2499
Async call from Promise.then
startReadingFromStream(), react-server-dom-webpack-client.browser.development.js:2506
exports.createFromReadableStream(), react-server-dom-webpack-client.browser.development.js:2718
createFromReadableStream(), app-index.tsx:157
(app-pages-browser)/./node_modules/next/dist/client/app-index.js(), main-app.js:160
options.factory(), webpack.js:700
__webpack_require__(), webpack.js:37
fn(), webpack.js:357
require(), app-next-dev.ts:9
hydrate(), app-bootstrap.ts:78
hydrate(), app-bootstrap.ts:20
loadScriptsInSequence(), app-bootstrap.ts:60
appBootstrap(), app-next-dev.ts:8
(app-pages-browser)/./node_modules/next/dist/client/app-next-dev.js(), main-app.js:182
options.factory(), webpack.js:700
__webpack_require__(), webpack.js:37
__webpack_exec__(), main-app.js:2824
anonymous(), main-app.js:2825
webpackJsonpCallback(), webpack.js:1376
anonymous(), main-app.js:9
And since all these scripts point to the same source map, this causes JS Debuggers to set redundant breakpoints.
I made some research and that's probably caused by React Server components implementation.
Since part of a component is executed on server, React is trying to recreate stack traces for some operations in a browser. One of the examples is console.log replay:
Here, to show a correct source location in the console during replay on the client, React use these techniques with fake scripts and source maps.