DEV Community

Cover image for How modern dev servers decide what to rebuild - a minimal engine
Alessio Pelliccione
Alessio Pelliccione

Posted on • Originally published at alessiopelliccione.Medium

How modern dev servers decide what to rebuild - a minimal engine

by Alessio Pelliccione

Some days ago, I was writing a small dynamic script to trigger a build for a single file. While doing that, I suddenly wondered: how do modern build tools actually work under the hood?

That question led me to start building a minimal engine — something simple enough to grasp the core idea, but realistic enough to mirror how tools like esbuild or Vite handle rebuilds internally, deciding what to rebuild.

Let’s see the process step by step — from file watching to dependency graph validation — to see what really happens when you hit “Save”.


What’s actually happening under the hood

When you hit “Save”, your dev server doesn’t rebuild the whole project.

Instead, it follows a smart and very fast chain of steps:

  1. The file watcher fires an event — tools like chokidar detect which file changed.
  2. The server looks up that file in its dependency graph — a map of who imports what.
  3. It invalidates the module and all the modules that depend on it.
  4. Only those affected modules are rebuilt and reloaded.

That’s it — no full recompilation, no walking through the entire codebase.

This is what makes tools like Vite and esbuild feel instant during development.


Building a minimal smart rebuild engine (with TypeScript code)

Let’s recreate the core idea behind the tools that I mentioned before — but in less than 40 lines of code. The requirements are simple:

  • watch files
  • detect changes
  • compute their hashes
  • rebuild only what’s affected

Step 1 — Watch the filesystem

We will use chokidar to detect changes in the src/ directory:

import chokidar from 'chokidar';

const watcher = chokidar.watch('src');
watcher.on('change', (file) => {
  console.log(' File changed:', file);
});
Enter fullscreen mode Exit fullscreen mode

Every time you save a file, this watcher emits an event.

Step 2 — Cache file hashes

We don’t want to rebuild if the content didn’t really change (auto‑formatting, whitespace edits). So let’s keep a hash map of file contents:

import crypto from 'crypto';
import fs from 'fs';

const cache = new Map<string, string>();
function hashFile(path: string) {
  const content = fs.readFileSync(path, 'utf‑8');
  return crypto.createHash('sha1').update(content).digest('hex');
}
Enter fullscreen mode Exit fullscreen mode

Now we can compare the old and the new hash whenever a file changes.

Step 3 — Track dependencies

We’ll store a simple dependency graph to know which files import which:

const graph = new Map<string, string[]>(); // file → [importers]
// Example:
graph.set('src/utils/date.ts', ['src/app.ts']);
graph.set('src/app.ts',    ['src/main.ts']);
Enter fullscreen mode Exit fullscreen mode

This tells us that if date.ts changes, app.ts and then main.ts should be invalidated.

Step 4 — Invalidate affected modules

Here’s the rebuild logic — recursive invalidation of dependents:

function invalidate(file: string) {
  console.log('♻️ Rebuilding', file);
  const dependents = graph.get(file) || [];
  dependents.forEach(invalidate);
}
Enter fullscreen mode Exit fullscreen mode

Now we connect everything:

watcher.on('change', (file) => {
  const newHash = hashFile(file);
  if (cache.get(file) !== newHash) {
    cache.set(file, newHash);
    invalidate(file);
  }
});
Enter fullscreen mode Exit fullscreen mode

And that’s it! You just built the heart of a smart rebuild engine.

The real implementation in my repo is a bit more advanced: it keeps a full dependency graph, incremental cache, and diagnostics for rebuild chains.

You can explore it here: https://github.com/alessiopelliccione/reboost


Understanding HMR (Hot Module Replacement) propagation

Now that our minimal engine knows which modules to rebuild, the next question is: how does the browser update so fast — without a full reload?

The idea

Instead of refreshing the entire page, HMR lets the browser replace only the affected module at runtime. Think of it like a “live patch” system for JavaScript.

When a file changes:

  1. The dev server detects it (our watcher)
  2. It invalidates the affected modules
  3. It sends a WebSocket message to the browser
  4. The browser dynamically imports the new module
  5. If the module accepts the update, it’s replaced in place — no full reload

How it connects back to Reboost

In Reboost, this concept becomes even more transparent: each module knows who depends on it through the dependency graph, and when a file changes, the invalidation chain determines how far the rebuild should go before notifying the browser.


Beyond this article

This post only scratches the surface.

My project Reboost is a small experimental engine that goes a step further. It’s not meant to replace modern dev servers, but to understand how they work.


Links


Written by Alessio Pelliccione

Top comments (0)