|$ curl https://forge-ai.dev/api/markdown?path=docs/build-tools/dev-servers
$cat docs/build-tools-—-dev-servers-&-hmr.md
updated Recently·10 min read·published

Build Tools — Dev Servers & HMR

Build ToolsIntermediate
Introduction

A development server is a local HTTP server that serves your application during development with features like hot module replacement, live reloading, and proxy configuration. The quality of your dev server directly impacts developer productivity — a slow dev server can waste hours of developer time each week.

Modern dev servers have evolved from simple file watchers to sophisticated systems using native ESM, WebSocket-based HMR, and framework-specific fast refresh. This guide covers the architecture and configuration of the most popular dev servers.

Development Server Basics

All dev servers share a common set of features, though the implementation details vary significantly. Understanding these fundamentals helps you choose the right dev server and configure it effectively.

FeatureVite Dev Serverwebpack-dev-serverNext.js Dev
Bundleresbuild (dev) / Rollup (build)WebpackWebpack / Turbopack
HMR approachESM native (no bundling)Module graph patchingModule graph patching
Cold startInstant (~50ms)Slow (~2-10s)Fast (~300ms-1s)
HMR latency~10ms~50-200ms~10-100ms
Proxy supportBuilt-inBuilt-inBuilt-in (rewrites)
HTTPSBuilt-inBuilt-inBuilt-in

info

Cold start speed matters more than you think. If a dev server takes 5 seconds to start and you restart it 20 times per day, that is 100 seconds per day of waiting. Over a year, a slow dev server wastes over 10 hours of developer time.
Vite Dev Server

Vite's dev server is fundamentally different from Webpack-based servers. Instead of bundling your entire application on startup, Vite serves ES modules directly to the browser. It only transforms individual files on request using esbuild for transpilation.

vite.config.js
JavaScript
1// vite.config.js — dev server configuration
2import { defineConfig } from "vite";
3import react from "@vitejs/plugin-react";
4
5export default defineConfig({
6 server: {
7 // Dev server port
8 port: 3000,
9 // Automatically open browser
10 open: true,
11 // Enable CORS for API requests
12 cors: true,
13 // Host for network access (0.0.0.0 for Docker)
14 host: "0.0.0.0",
15 // HMR configuration
16 hmr: {
17 // Custom WebSocket port
18 port: 34658,
19 // Protocol (ws or wss for HTTPS)
20 protocol: "ws",
21 // Timeout for HMR connection
22 timeout: 5000,
23 },
24 // Watch options for file system
25 watch: {
26 // Use polling (useful for Docker/NFS)
27 usePolling: false,
28 // Interval for polling
29 interval: 100,
30 },
31 },
32 // Dependency optimization (pre-bundling)
33 optimizeDeps: {
34 include: ["react", "react-dom", "lodash-es"],
35 exclude: ["large-lib-with-many-files"],
36 },
37 plugins: [react()],
38});

info

Vite's optimizeDeps.exclude is useful for large libraries that have many internal files. Pre-bundling such libraries can slow down cold start. Exclude them and let the browser handle individual module loads directly.
webpack-dev-server

webpack-dev-server serves your application from memory (not disk) and provides live reloading and HMR. It is the most mature dev server with extensive configuration options, though it is significantly slower than Vite for large projects.

webpack.config.js
JavaScript
1// webpack.config.js — dev server configuration
2const path = require("path");
3const HtmlWebpackPlugin = require("html-webpack-plugin");
4
5module.exports = {
6 mode: "development",
7 devtool: "eval-cheap-module-source-map", // Fast source maps
8 entry: "./src/index.js",
9 output: {
10 path: path.resolve(__dirname, "dist"),
11 publicPath: "/",
12 },
13 devServer: {
14 port: 3000,
15 hot: true, // Enable HMR
16 liveReload: true, // Fallback to full reload if HMR fails
17 open: true, // Open browser
18 historyApiFallback: true, // SPA fallback for client-side routing
19 compress: true, // gzip compression for faster loading
20 // Static file serving
21 static: {
22 directory: path.join(__dirname, "public"),
23 },
24 // Client configuration
25 client: {
26 overlay: {
27 errors: true, // Show errors in browser overlay
28 warnings: false, // Don't show warnings in overlay
29 },
30 logging: "warn", // Client log level
31 progress: true, // Show compilation progress
32 },
33 // Headers for development
34 headers: {
35 "X-Development-Mode": "true",
36 },
37 },
38 plugins: [
39 new HtmlWebpackPlugin({
40 template: "./public/index.html",
41 }),
42 ],
43};

warning

Avoid devtool: "source-map" in development. It rebuilds the entire source map on every change, significantly slowing HMR. Use "eval-cheap-module-source-map" for the best balance of speed and debuggability.
HMR Architecture

Hot Module Replacement (HMR) updates modules in the browser without a full page reload. It preserves application state, making it dramatically faster for development than live reloading. The HMR pipeline involves four stages: detection, notification, update, and acceptance.

hmr-architecture.js
JavaScript
1// HMR architecture — how it works under the hood
2//
3// 1. File change detected by watcher (chokidar, fs.watch)
4// 2. Build system re-compiles the changed module
5// 3. WebSocket message sent to browser with update manifest
6// 4. Browser runtime fetches updated module(s)
7// 5. HMR runtime applies the update (module.hot.accept)
8// 6. If HMR fails, fall back to full page reload
9
10// WebSocket message format (webpack-dev-server)
11{
12 type: "json",
13 action: "built",
14 hash: "a1b2c3d4",
15 errors: [],
16 warnings: [],
17 modules: [
18 { id: "./src/components/Header.tsx", hash: "e5f6g7h8" },
19 { id: "./src/styles.css", hash: "i9j0k1l2" },
20 ]
21}
22
23// Vite HMR uses native ESM under the hood
24// The browser fetches updated modules via regular import()
25// No module graph patching — just re-fetch the ESM file
26
27// Custom HMR handling in user code
28if (import.meta.hot) {
29 // Vite HMR API
30 import.meta.hot.accept((newModule) => {
31 // Called when this module is updated
32 render(newModule.default);
33 });
34
35 // Handle dispose — cleanup before re-render
36 import.meta.hot.dispose(() => {
37 destroyPreviousInstance();
38 });
39}
40
41// Webpack HMR API
42if (module.hot) {
43 module.hot.accept("./heavyComponent", () => {
44 const HeavyComponent = require("./heavyComponent").default;
45 render(<HeavyComponent />);
46 });
47}

info

Vite's ESM-based HMR is inherently faster than Webpack's module graph patching because it avoids the bundling step entirely. When a file changes, Vite only needs to transform that single file and send its URL to the browser. Webpack must re-bundle the affected module graph and send the patched modules.
React Fast Refresh

React Fast Refresh is a React-specific HMR implementation that preserves component state across edits. Unlike generic HMR, Fast Refresh understands React component boundaries and can re-render only the changed components while preserving the state of sibling and parent components.

react-fast-refresh.tsx
JavaScript
1// React Fast Refresh — how it works
2// Only re-renders the edited component, preserving:
3// - Component state (useState, useReducer)
4// - Context values
5// - Refs
6// - Effect cleanup and re-run
7
8// State preserved after edit:
9function Counter() {
10 const [count, setCount] = useState(0);
11 // If you edit this component, count is preserved!
12 return <button onClick={() => setCount(count + 1)}>{count}</button>;
13}
14
15// State RESET after edit (class component or HOC):
16// Class components always re-mount on edit
17class Counter extends React.Component {
18 state = { count: 0 };
19 // Editing this class component will RESET count to 0
20}
21
22// State RESET after edit (anonymous default export):
23export default function Counter() {
24 // Because this is an anonymous function,
25 // Fast Refresh cannot track it — state WILL reset
26}
27
28// State preserved — named function component:
29export default function Counter() {
30 // Named function — Fast Refresh can track it
31 // State is PRESERVED after edits
32}
33
34// Configuring Fast Refresh in Vite
35// @vitejs/plugin-react enables Fast Refresh automatically
36import react from "@vitejs/plugin-react";
37
38export default defineConfig({
39 plugins: [
40 react({
41 fastRefresh: true, // Enabled by default
42 // Exclude components from Fast Refresh
43 exclude: /\/src\/vendor\//,
44 }),
45 ],
46});

warning

Fast Refresh only works with function components. Class components always re-mount on edit, losing their state. If you are migrating to Hooks, use named function components for optimal Fast Refresh behavior. Anonymous default exports also break Fast Refresh.
Dev Server Proxy Configuration

Dev server proxies forward API requests from your development domain to a backend server. This avoids CORS issues during development and mirrors production where API and frontend share a domain.

proxy-configuration.js
JavaScript
1// Vite proxy configuration
2// vite.config.js
3export default defineConfig({
4 server: {
5 proxy: {
6 // Forward all /api requests to backend
7 "/api": {
8 target: "http://localhost:8000",
9 changeOrigin: true,
10 // Rewrite path: remove /api prefix
11 rewrite: (path) => path.replace(/^\/api/, ""),
12 },
13 // Forward WebSocket connections
14 "/ws": {
15 target: "ws://localhost:8000",
16 ws: true, // Enable WebSocket proxy
17 },
18 // Forward with authentication header
19 "/graphql": {
20 target: "http://localhost:4000",
21 changeOrigin: true,
22 headers: {
23 "X-Dev-Token": "development-token",
24 },
25 },
26 },
27 },
28});
29
30// webpack-dev-server proxy configuration
31// webpack.config.js
32module.exports = {
33 devServer: {
34 proxy: [
35 {
36 context: ["/api", "/auth"],
37 target: "http://localhost:8000",
38 changeOrigin: true,
39 secure: false, // Allow self-signed certificates
40 pathRewrite: { "^/api": "" },
41 // Bypass proxy for certain paths
42 bypass: (req) => {
43 if (req.headers.accept?.includes("text/html")) {
44 return "/index.html"; // Serve index.html for SPA routes
45 }
46 },
47 // Handle proxy errors
48 onError: (err, req, res) => {
49 res.writeHead(500, { "Content-Type": "text/plain" });
50 res.end("Proxy error: " + err.message);
51 },
52 },
53 // HTTPS proxy to backend
54 {
55 context: ["/secure-api"],
56 target: "https://api.example.com",
57 secure: true,
58 headers: {
59 "X-Forwarded-Host": "localhost:3000",
60 },
61 },
62 ],
63 },
64};

info

When working with HTTPS backends in development, set secure: false to allow self-signed certificates. In production, your proxy should always validate certificates. Never deploy development proxy configurations to production.
Dev Server Performance Tips

Dev server performance directly impacts developer productivity. These tips help keep your dev server fast as your project grows.

  • Use native ESM dev servers (Vite) — For new projects, choose Vite over Webpack for 10-100x faster cold starts.
  • Exclude large files from watch — Configure server.watch.ignored to skip node_modules, build output, and generated files.
  • Use fast source maps"eval-cheap-module-source-map" (Webpack) or no source maps in dev for maximum speed.
  • Lazy compile routes — Configure lazy compilation so only the current route's modules are compiled on startup.
  • Disable HMR for specific modules — Some third-party modules don't support HMR. Disable it for those specific modules.
  • Limit file watching scope — Use server.watch.ignored to exclude **/node_modules/**, **/.git/**, **/dist/**.
perf-config.js
JavaScript
1// Vite — performance-focused configuration
2export default defineConfig({
3 server: {
4 watch: {
5 // Exclude large directories from file watching
6 ignored: [
7 "**/node_modules/**",
8 "**/dist/**",
9 "**/.git/**",
10 "**/__generated__/**",
11 "**/*.snap",
12 "**/coverage/**",
13 ],
14 },
15 // Pre-transform static imports for faster HMR
16 preTransformRequests: true,
17 },
18 // Exclude large dependencies from optimization
19 optimizeDeps: {
20 exclude: ["large-vendor-lib"],
21 },
22 // Cache pre-bundled dependencies
23 cacheDir: "node_modules/.vite-cache",
24 build: {
25 // Skip type checking during dev for speed
26 // (do it in a separate process or in CI)
27 },
28});
29
30// Webpack — performance configuration
31module.exports = {
32 // Fastest source map for development
33 devtool: "eval-cheap-module-source-map",
34 watchOptions: {
35 ignored: /node_modules/,
36 aggregateTimeout: 200, // Debounce file changes
37 poll: false, // Disable polling (use OS events)
38 },
39 // Limit compilation scope
40 module: {
41 // Exclude node_modules from loaders (most are pre-compiled)
42 noParse: /lodash|moment/,
43 },
44};
$Blueprint — Engineering Documentation·Section ID: BT-DEV·Revision: 1.0