vercel / vercel/next.js

Dev HMR client reconnects forever after prerender activation (visibilitychange while socket is CONNECTING)

Open Beginner friendly
#98,212 0 comments 0 reactions 0 assignees View on GitHub

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
  1. npm install && npm run dev
  2. In Chrome open http://localhost:3000. The page has a Speculation Rules prerender for /target.
  3. Wait a second, click the link to /target (or open /?auto, which navigates after 2.5s).
  4. 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):

  • handleVisibilityChange calls init() when readyState !== OPEN. CONNECTING is not OPEN, so activation of a prerendered document fires it mid-handshake.
  • init() closes the previous socket without clearing its onclose/onerror. That socket's handleDisconnect schedules init() again in 1s, which closes the new socket, and so on. reconnections resets on every onopen, so WEB_SOCKET_MAX_RECONNECTIONS never 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.