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 devprocess was interrupted (Ctrl+C) but left child processes holding the socket. - Stray Turborepo Processes: If you see
grpcerrors 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.tsrequires exclusive access to port 8081 to broadcastdo_updatesignals. - Restarting
pnpm devforces the server to re-bind, provided no lingering process occupies the port. - Killing stray
turboprocesses 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →