DEV Community

RAXXO Studios
RAXXO Studios

Posted on • Originally published at raxxo.shop

Replacing Webpack DevServer With Bun Hot Reload: 14 Lines, 3.2x Faster

  • Webpack DevServer ate 250MB and took 4.1s to start

  • 14 lines of Bun replaced it with WebSocket HMR

  • Cold start dropped from 4.1s to 1.3s, memory from 250MB to 40MB

  • It still loses on React Fast Refresh, CSS HMR, and proxy config

I had a small frontend project. Plain JS, a few HTML files, one CSS file, no React. Webpack DevServer was running it, eating 250MB of memory and taking 4.1 seconds to start. I replaced the whole thing with 14 lines of Bun. Here is what worked, what broke, and the numbers.

The Webpack DevServer I Was Actually Running

The project was a static site with maybe 30 KB of source. Index.html, a single bundle.js, one stylesheet, and three image assets. The Webpack config was 47 lines. The dev dependency tree was 312 packages. node_modules weighed in at 184 MB.

Why was Webpack there at all? Because three years ago I started the project from a template, and the template had Webpack, and I never questioned it. The template gave me hot module replacement, a dev server, source maps, and a build step. I used the dev server every day. I used the build step once a week. The other 90% of the config was unused.

When I ran npm run dev, this is what happened. webpack-cli loaded. webpack-dev-server loaded. Plugins loaded. The compiler ran a full pass over my 30 KB of source, which Webpack treated as a graph problem. Memory climbed to 250 MB. After 4.1 seconds, the server was listening on port 8080.

The HMR worked, but it was loud. Every save triggered a recompile, a hash check, a payload over WebSocket, and a webpackHotUpdate call in the browser. Latency from save to paint was around 320 ms on my machine. Not slow enough to be painful. Slow enough to notice.

The real problem was not speed. It was that I was loading a tool built for a 50,000-line codebase to serve a 30 KB static site. The mismatch bothered me every time I saw the memory chart.

I had Bun installed already. Bun ships with a file watcher, an HTTP server, native WebSocket support, and a static file handler. Everything I needed for a dev server was sitting in the runtime. I just had to wire it up.

The 14-Line Bun Replacement

Here is the file. I named it dev.js and dropped it next to index.html.


import { watch } from "fs";

const clients = new Set();
const root = "./public";

Bun.serve({
  port: 3000,
  async fetch(req, server) {
    if (server.upgrade(req)) return;
    const path = new URL(req.url).pathname;
    const file = Bun.file(root + (path === "/" ? "/index.html" : path));
    return new Response(file);
  },
  websocket: { open(ws) { clients.add(ws); }, close(ws) { clients.delete(ws); } }
});

watch(root, { recursive: true }, () => clients.forEach(c => c.send("reload")));
console.log("dev server on http://localhost:3000");

Enter fullscreen mode Exit fullscreen mode

Fourteen lines. Zero dependencies beyond Bun itself. It serves static files from ./public, upgrades any matching request to a WebSocket, and pushes a reload message to every connected client whenever a file changes.

The client side needs three lines in index.html.


  new WebSocket("ws://localhost:3000").onmessage = () => location.reload();

Enter fullscreen mode Exit fullscreen mode

That is the entire HMR layer. It is not module replacement, it is a full reload. For a static site with three JS files and one stylesheet, full reload is fast enough that the difference is invisible. The browser caches assets, the WebSocket round trip is sub-millisecond on localhost, and the page is back on screen before my eyes leave the editor.

The script tag only runs in dev because the production build strips it. In my case the production build is cp -r public dist, so I skip the strip and just serve a different index.html from the static host. Two index.html files, one with the script, one without. Done.

I deleted Webpack, webpack-cli, webpack-dev-server, html-webpack-plugin, css-loader, style-loader, mini-css-extract-plugin, and seven Babel packages. node_modules dropped from 184 MB to 0 MB, because I had no remaining dev dependencies. The package.json devDependencies block went from 23 entries to zero.

Numbers: Cold Start, HMR Latency, Memory

I ran both setups ten times each on the same machine, M1 MacBook, nothing else open. Median values.

Cold start. Time from npm run dev (or bun dev.js) to the server accepting its first request.

  • Webpack DevServer: 4.1 seconds

  • Bun: 1.3 seconds

  • Speedup: 3.2x

HMR latency. Time from save in the editor to the page being repainted in the browser. Measured with a console.timeEnd on the client.

  • Webpack DevServer (HMR): 320 ms

  • Bun (full reload): 80 ms

  • Speedup: 4x

The Bun number is faster even though it does a full reload, because the full reload over a hot WebSocket beats Webpack's incremental update path for small bundles. Webpack pays the cost of recompiling, hashing, and sending a JSON payload. Bun pays the cost of one WebSocket message and a browser refresh. For 30 KB of assets, the refresh wins.

Memory. Resident memory of the dev server process after it has been running for two minutes with one save event.

  • Webpack DevServer: 251 MB

  • Bun: 39 MB

  • Reduction: 6.4x

Disk. node_modules size for dev dependencies only.

  • Webpack stack: 184 MB

  • Bun: 0 MB

The disk number is the one I felt most. Cloning the repo on a fresh machine used to take 14 seconds of npm install. Now it takes zero. Bun is the only dependency, and it is installed globally.

These numbers are for a small project. They do not extrapolate to a 50,000-line app with code splitting and CSS modules. They do extrapolate to every other small static site I run, of which I have eleven.

Where This Setup Still Loses to Webpack/Vite

I am not telling anyone to delete their Webpack config. Here is what I gave up.

No React Fast Refresh. Webpack and Vite both preserve component state across saves. Edit a component, the form values stay filled in, the open modal stays open. Bun's full reload throws all that away every time. For a JSX-heavy project this is a real loss. I do not write JSX in this project, so I do not feel it. If you do, stay on Vite.

No CSS HMR. Vite injects new CSS without reloading the page. My setup does a full reload on every CSS change, which means scroll position is lost and any client-side state evaporates. For a one-page static site this is fine. For a multi-step form, it is annoying. PostCSS chains, Tailwind, CSS modules, all of these need real HMR to feel right, and a 14-line script will not give you that.

No module graph. Webpack and Vite know which files import which. When you edit a leaf module, only the modules that depend on it get re-evaluated. My setup reloads the world on any file change. For a 30 KB project this is invisible. For a 30 MB project it would feel slow.

No proxy config. webpack-dev-server has a proxy field that forwards /api/* to a backend during dev, dodging CORS. My 14 lines do not. If your dev workflow needs proxying, you have to write that yourself, and the budget grows past 14 lines fast.

No source maps for bundled code. Because there is no bundler, there is nothing to source-map. If you ship one bundled JS file in production, you lose the dev map. I ship raw ES modules, so this does not apply, but if you bundle, this matters.

No TypeScript transpile. Bun runs TS natively on the server, but the browser does not. If you need TS in browser code, you need a build step or a runtime transpiler, and that pulls more lines into the picture.

The honest summary: this setup is great for static sites, ESM-only projects, and small experiments. It is wrong for any app that needs React Fast Refresh, CSS HMR, a proxy, or a real module graph.

Bottom Line

Fourteen lines of Bun replaced 23 dev dependencies, 184 MB of node_modules, and a Webpack config I had not touched in three years. Cold start went from 4.1 seconds to 1.3 seconds, a 3.2x improvement. HMR latency dropped from 320 ms to 80 ms, even with a full reload, because the full reload over a hot WebSocket beats incremental updates for tiny bundles. Memory dropped from 251 MB to 39 MB.

This is not a Vite replacement. It is not a Webpack replacement. It is the right tool for static sites and ESM-only projects where the build step is cp -r public dist and the module graph fits in your head. For everything else, keep Vite.

I rolled this out across eleven small projects in a weekend. Cumulative dev memory savings, around 2.3 GB. Cumulative cold start savings per day, around 30 seconds.

If you liked this teardown, the Bun Shell article in the same cluster covers replacing bash scripts with Bun.$. Same energy, smaller surface, bigger payoff.

Built and tested by RAXXO Studios. More tools and write-ups at raxxo.shop.

Top comments (0)