How to Fix "Hot Module Reload Seems to Have Frozen" in the Chrome Extension React Vite Boilerplate

Kill any process bound to port 8081 and restart pnpm dev to clear the WebSocket conflict and restore live reloading.

The jonghakseo/chrome-extension-boilerplate-react-vite repository ships a custom Hot Module Replacement (HMR) system that coordinates across background service workers, UI pages, and content scripts. When this pipeline breaks, edits to your source files no longer trigger automatic browser refreshes, leaving the extension stuck with stale code until you manually intervene.

Understanding the HMR Architecture

The boilerplate implements a three-part reload pipeline that bridges Vite’s build system with Chrome’s extension runtime. According to the source code in packages/hmr/, the flow works as follows:

WebSocket Server (init-reload-server.ts)

Located at packages/hmr/lib/initializers/init-reload-server.ts (lines 15–42), this component creates a WebSocket listener on port 8081. When Vite finishes rebuilding the bundle, the server broadcasts a do_update message to all connected clients. If the port is already occupied, the code explicitly logs an EADDRINUSE warning and aborts startup (lines 45–49), preventing the server from ever sending reload signals.

Client Initializer (init-client.ts)

The packages/hmr/lib/initializers/init-client.ts file (lines 1–18) runs inside the extension context. It opens a persistent WebSocket connection to ws://localhost:8081, listens for the do_update event, executes the provided update callback, and acknowledges completion by sending done_update back to the server.

Reload Injection (reload.ts)

The packages/hmr/lib/injections/reload.ts file (lines 1–13) contains the runtime code injected into the extension bundle. When the client initializer receives an update signal, this script calls chrome.runtime.reload() to refresh the entire extension or triggers module-specific updates for content scripts.

Root Cause of the Freeze

The "frozen" state occurs when the WebSocket server cannot bind to port 8081, most commonly because a previous development instance is still running. Since the client-side code in init-client.ts never receives the do_update message without an active server connection, the extension remains unaware of file changes.

This conflict typically surfaces in two scenarios:

  • Orphaned Dev Server: A pnpm dev process was interrupted (Ctrl+C) but left child processes holding the socket.
  • Stray Turborepo Processes: If you see grpc errors in the terminal, a previous Turborepo (turbo) instance is still managing the monorepo pipeline and occupying the HMR port.

Step-by-Step Recovery

Use these terminal commands to clear the port conflict and restore HMR functionality.

Method 1: Clean Server Restart

First, stop the current development session. If the terminal is unresponsive, force-kill the process:


# Stop the current dev server

pkill -f "pnpm dev"

# Alternatively, kill by port

kill $(lsof -t -i:8081)

# Restart the development environment

pnpm dev

Method 2: Kill Stray Turborepo Processes

When the freeze coincides with gRPC errors, terminate the lingering turbo process before restarting:


# Remove the stray process holding the HMR socket

pkill -f "turbo"

# Launch a fresh instance

pnpm dev

Method 3: One-Liner Port Reset

For a single command that clears port 8081 and immediately restarts development:

kill $(lsof -t -i:8081) && pnpm dev

Key Configuration Files

These source files define the HMR behavior and are useful for debugging custom modifications:

File Purpose
packages/hmr/lib/consts.ts Defines the WebSocket port (8081) and message type constants (do_update, done_update).
packages/hmr/lib/initializers/init-reload-server.ts Initializes the WebSocket server and handles connection cleanup.
packages/hmr/lib/initializers/init-client.ts Manages the client-side WebSocket connection and update callbacks.
packages/hmr/lib/injections/reload.ts Contains the chrome.runtime.reload() trigger injected into the extension bundle.

Summary

  • The HMR freeze is a port-conflict issue, not a code error.
  • The WebSocket server in init-reload-server.ts requires exclusive access to port 8081 to broadcast do_update signals.
  • Restarting pnpm dev forces the server to re-bind, provided no lingering process occupies the port.
  • Killing stray turbo processes resolves gRPC-related conflicts that prevent the server from starting.
  • The reload pipeline depends on three coordinated components: the server initializer, the client initializer, and the runtime injection script.

Frequently Asked Questions

What specific port does the HMR server use?

The server listens on port 8081, as defined in packages/hmr/lib/consts.ts. If this port is unavailable, the server logs an EADDRINUSE error and the extension never receives reload notifications.

Why does Turborepo cause the HMR to freeze?

Turborepo manages the monorepo pipeline across multiple packages. If a previous turbo process crashes or hangs, it continues holding the WebSocket port, blocking new instances from broadcasting do_update messages to the extension client.

How can I verify what process is blocking port 8081?

Run lsof -i:8081 on macOS/Linux or netstat -ano \| findstr :8081 on Windows to identify the process ID. Once identified, terminate it with kill <PID> (Unix) or taskkill /PID <PID> /F (Windows) before restarting pnpm dev.

Will killing the turbo process affect other projects?

Terminating a stray turbo process only affects the specific monorepo instance that was frozen. Other active Turborepo sessions on different ports or directories will continue running normally. Always verify the process ID with lsof to ensure you are targeting the correct instance.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →