article > tech
How a Local Development Server Works in React Web Frontend
What happens when you run the dev command and connect to localhost
Generally, bundlers provide a local development server. webpack comes with webpack-dev-server, and vite has plugin-react. Developers don’t usually need to worry about what happens when they execute dev or serve commands because of the high level of abstraction.
However, in the Micro Frontend framework podojs, which I maintain at work, using a compatible development server from the bundler was quite painful, even after significant modifications. I’ll discuss the exact issues at another time. To solve these problems better, I decided to study local development servers.
In this post, we’ll create a very basic local development server that supports live-reload and explore the fundamental workings and components of a local development server.
Overview
Refer to the example code for the actual operations of each implementation.

- Modify code in an IDE (e.g., vscode).
- A watcher monitoring a specific directory triggers the bundler.
- Once the bundler successfully builds, it sends a message to the React app running in the web browser via a web socket.
- The React app, upon receiving the web socket message, refreshes the browser.
- After the refresh, the dev-server requests the bundled output to redraw the app.
Components
In this section, I will explain each component in the diagram. I’ll also summarize what I know from open-source bundlers (mostly webpack).
1. Watcher
Monitors changes, additions, and deletions in all files under a specific directory to trigger bundling by the bundler.
Typically, you can watch the entire file tree that depends on the client’s bundle entry point (webpack.entry), but in a monorepo setup, you might expand the watch scope to include all internal packages the application depends on.
watchpack, a lower-level library of webpack, detects directories involved in bundling and attaches fs.watch to them. Depending on the scale of files being watched, there’s an algorithm that decides whether to attach a watcher to the directory or to each file individually.
I used to think that attaching watchers before bundling was delegated to webpack-dev-middleware or webpack-dev-server, but after checking the code, I found that webpack uses watchpack directly in its default plugin operations.
In the webpack config file, you can control this webpack.watch through the watchOptions property. The ignored property is particularly useful, as it allows you to pass glob patterns of directories or files not to watch. Too many watchers can cause performance issues or prevent proper watching and rebundling.
parcel provides its watcher as an independent library, @parcel/watcher. The example code uses this.
2. Bundler
Bundling is sometimes roughly referred to as building or compiling. Internally, webpack refers to a bundler instance as a Compiler. However, since building and compiling are different processes than bundling, I prefer the term bundling.
Intuitively, when you run a development server and modify code, the bundling results would continue to accumulate, which would be inefficient.
In a typical local development server, an in-memory file system like memfs is used to create files in memory without actual read/write operations on the node.fs file system. In webpack, you can set this behavior through the outputFileSystem option.
When the watcher detects valid file changes, it calls the bundler instance to create a new build output matching the latest code state. webpack-dev-middleware aims to integrate these operations in a node server.
Alternatively, as shown in the example code, if you can programmatically control the creation and invocation of the bundler instance, you can integrate it directly with the watcher as follows:
watcher.subscribe(async (eventLogs) => {
await bundler.run();
});
3. Server
This refers to the server that serves the bundled output to the browser. Typically, it uses the same host and port as the localhost used by the browser. This ensures that when resources are requested with relative paths in the browser, they correctly reach the local development server’s resources.
<!-- Request resources from localhost:3000/main.js -->
<script src="main.js" />
If you use webpack-dev-server, it also supports specific path settings.
Using html-webpack-plugin automatically adds script tags to serve the entry bundle in index.html, making good use of the exposed server endpoints. Other bundlers support similar operations.
In the example code, memfs is used to access the bundled output, allowing all resources to be requested directly under the / endpoint.
4. Web-Socket
In a local development environment, the server and browser need to synchronize actions like code changes and reloading the newly bundled output. Therefore, the browser must be aware of some server states in real-time. Upon initial connection to the localhost, the browser establishes a web socket connection with the local development server.
In the example code, after code changes and the completion of rebundling, the server sends a socket message to the browser, which then refreshes the page.
The following code summarizes the core actions of a local development server:
// Detect file changes
watcher.subscribe(async (eventLogs) => {
// Send a socket message to the browser indicating file changes
socket.send({
type: 'changeDetected',
name,
files: eventLogs,
});
// Re-invoke the bundler instance
await bundler.run();
// After bundling, send a success message to the browser
socket.send({
type: 'compileSuccess',
name,
});
});
webpack-dev-server also uses web sockets. It has options related to web sockets, and the browser and server synchronize states via web sockets as shown below.

5. Client-Runtime
To support live-reload, the browser must refresh when bundling is complete. As seen above, the browser should refresh upon receiving a socket message indicating bundling completion.
Such behavior must be evaluated in the browser when connecting to the localhost. The browser in a local development environment requires client runtime code to initialize and set up necessary operations.
An example of this is code that performs specific actions upon receiving certain web socket messages, as shown in the example code. Here, the script code was included in the initially served HTML.
When setting up a local development server using webpack-dev-middleware, which is one level of abstraction lower than webpack-dev-server, you need to add client bundles as entry points in the webpack config to be evaluated in the browser.
Fast-refresh, which allows continuous evaluation of changed bundles while maintaining state, requires additional runtime code for operation. In vite’s react-plugin that supports fast-refresh, you can see that certain runtime code is added before and after the newly bundled module code when it’s evaluated in the browser.
Conclusion
Although I briefly mentioned it in this post, I’ll cover the implementation levels of local development servers (Live Reload, Hot Module Replacement, fast-refresh) in the next post.
References
- webpack-dev-server
- webpack-dev-middleware
- @vitejs/plugin-react
- # Mental model for the new dev flow 🧠 - Pedro Cattori
(End)