Dev HMR client reconnects forever after prerender activation (visibilitychange while socket is CONNECTING)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.4k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Where I found this
Chat app on a custom server, next() plus ws, started with npm run dev. Cold tab on localhost:8080, chat never connects. Server log is nothing but /_next/hmr upgrades, one a second. No call to our ticket endpoint, so the client code never ran. Refresh: works. Close the tab, open a new one, same URL: broken again. Console shows WebSocket is closed before the connection is established, then [HMR] connected 245 times. Chrome had "Preload pages" on. Prod build never does this.
Link to the code that reproduces this issue
https://github.com/hairywelly/next-hmr-visibility-repro
To Reproduce
npm install && npm run dev- In Chrome open http://localhost:3000. The page has a Speculation Rules
prerenderfor/target. - Wait a second, click the link to
/target(or open/?auto, which navigates after 2.5s). - Open the console on
/target.
Control: open http://localhost:3000/target directly.
Current vs. Expected behavior
Current: WebSocket connection to 'ws://localhost:3000/_next/hmr?id=…' failed: WebSocket is closed before the connection is established, then [HMR] connected again every second, indefinitely. performance.getEntriesByType('navigation')[0].activationStart is > 0 on the broken page and 0 on the control.
Expected: one HMR connection, as on the control page.
If the killed first socket had already reached the server it also takes the React debug channel with it (debug-channel.ts hands it out once per request id), and the page never hydrates. That is the case above: the server logged two /_next/hmr upgrades before the loop started.
Measured with headless Chrome over CDP, 16.3.3 and 16.4.0-canary.15: prerendered load opens 8 new HMR sockets in 8s; direct load opens 0.
Cause
packages/next/src/client/dev/hot-reloader/app/web-socket.ts (since #91416):
handleVisibilityChangecallsinit()whenreadyState !== OPEN. CONNECTING is not OPEN, so activation of a prerendered document fires it mid-handshake.init()closes the previous socket without clearing itsonclose/onerror. That socket'shandleDisconnectschedulesinit()again in 1s, which closes the new socket, and so on.reconnectionsresets on everyonopen, soWEB_SOCKET_MAX_RECONNECTIONSnever trips.
Fix that works locally:
function init() {
if (webSocket) {
+ webSocket.onclose = null
+ webSocket.onerror = null
webSocket.close()
}
...
function handleVisibilityChange() {
- if (document.visibilityState === 'visible' && webSocket.readyState !== WebSocket.OPEN) {
+ if (document.visibilityState === 'visible' && webSocket.readyState === WebSocket.CLOSED) {
Happy to open a PR.
Verify canary release
- I verified that the issue exists in the latest Next.js canary release
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.6.0: Sat Jul 11 15:27:04 PDT 2026; root:xnu-12377.161.13~4/RELEASE_ARM64_T6050
Available memory (MB): 49152
Available CPU cores: 18
Binaries:
Node: 24.16.0
npm: 11.16.0
Yarn: N/A
pnpm: N/A
Relevant Packages:
next: 16.3.3 // There is a newer version (16.3.4) available, upgrade recommended!
eslint-config-next: N/A
react: 19.2.4
react-dom: 19.2.4
typescript: N/A
Next.js Config:
output: N/A
Which area(s) are affected?
Developer Experience, Turbopack
Which stage(s) are affected?
next dev (local)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/next/src/client/dev/hot-reloader/app/web-socket.ts and reproduce with the linked example using npm install && npm run dev, comparing a prerendered navigation with a direct load. Trace handleVisibilityChange, init, and handleDisconnect during the CONNECTING state; done means the target page maintains one HMR connection without repeated reconnects or preventing hydration.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nextjs, react
- Domain
- developer-experience, tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100