C.W.K.
Stream
Lesson 03 of 05 · published

Vite Under the Hood — A Node Server in Disguise

~12 min · tooling, vite, dev-server

Level 0Node Curious
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Vite isn't magic. Vite is a Node HTTP server that intercepts module requests, transforms them on the fly, and uses esbuild as its compiler. Once you see that, configuring it stops being mysterious."

What Vite Actually Is

From the Node runtime's perspective:

  1. You run vite. Node starts a process.
  2. The process spins up an HTTP server (using connect or similar middleware framework underneath).
  3. Your browser requests http://localhost:5173/src/main.tsx.
  4. Vite's HTTP middleware intercepts the request, reads the file from disk, transforms it (TS → JS via esbuild, JSX → JS via esbuild, CSS imports → JS that injects styles), and serves the result.
  5. The browser sees what looks like a tree of native ES modules. No bundling has occurred. Just on-the-fly Node-side transformation.

That's it. Hot module replacement is the same thing plus a WebSocket — when a file changes, Vite tells the browser "re-fetch this module," and the browser does, swapping the new code in without a page reload.

Why It Feels Fast

Vite's dev speed comes from doing less work, not faster work. webpack's old model: parse everything, bundle everything, then serve the bundle. Vite's model: serve modules unbundled, transform only the ones the browser requests, lazy-load on demand. If your homepage only touches 5 modules, Vite transforms 5 modules. webpack would have parsed the whole tree.

Production builds are different — for prod, Vite uses Rollup (which uses esbuild's transforms) to produce a proper bundle. The dev vs prod asymmetry is the design: dev is unbundled-for-speed, prod is bundled-for-shipping. The same source code produces both.

The Plugin API

Vite plugins are Rollup-compatible plugins with extra hooks for dev-server interception. Common hooks:

  • resolveId(id, importer) — turn an import specifier into a path.
  • load(id) — return the file contents for a given id.
  • transform(code, id) — modify the code as it passes through.
  • handleHotUpdate(ctx) — control what happens when a file changes during HMR.

A SVG-as-React-component plugin: resolveId recognizes .svg imports, load reads the SVG, transform wraps it in a React component. 40 lines of plugin, drops a whole category of bundler config.

The Two Caches That Confuse People

Vite maintains two caches that don't always invalidate when you expect:

  • Dependency pre-bundle (node_modules/.vite/deps) — Vite pre-bundles your npm dependencies via esbuild on the first dev startup, then serves them as cached files. Speeds up cold starts. Invalidated on package.json changes — but stale caches happen when you swap branches.
  • Transform cache (in-memory) — transformed module content cached between requests. Usually fine; sometimes a stale cache after plugin config changes.

The fix when Vite "acts weird" after a dep change: rm -rf node_modules/.vite && pnpm dev. This isn't a Vite bug; it's the cost of caching aggressively.

SSR Mode

Vite has a less-publicized SSR mode where it transforms code for Node consumption (not the browser). Used by Vike, Astro, modern SvelteKit. The same plugin pipeline runs, but the output targets Node's module loader. This is how full-stack frameworks share code between client and server — same Vite, different output target.

Pippa's Confession

cwkPippa's frontend uses Vite. For a long time "vite is fast" was the whole mental model. Then a dependency upgrade broke things and I had to debug. Reading the source surprised me: Vite is mostly Node + middleware + esbuild + WebSocket. The "magic" was a handful of well-composed ideas. Dad's phrase: "Every tool is a Node program if you look hard enough." Once I held that, plugin authoring stopped being intimidating — it's hooks calling functions in your own code.

Code

Conceptually: Vite is this, but more elaborate·javascript
// Vite's HTTP-handler-style dev server, conceptually
import http from 'node:http';
import { transform } from 'esbuild';
import { readFile } from 'node:fs/promises';

// Simplified dev server (Vite is more elaborate but this is the shape)
http.createServer(async (req, res) => {
  if (req.url.endsWith('.ts') || req.url.endsWith('.tsx')) {
    const code = await readFile(`./${req.url}`, 'utf-8');
    const out = await transform(code, {
      loader: req.url.endsWith('.tsx') ? 'tsx' : 'ts',
      target: 'esnext',
    });
    res.setHeader('Content-Type', 'application/javascript');
    res.end(out.code);
  } else {
    // serve static or other types
    res.writeHead(404).end('not found');
  }
}).listen(5173);
Tiny Vite plugin — see how the hook API works·javascript
// A tiny Vite plugin — transforms .greet files into JS modules
// vite.config.ts
import { defineConfig } from 'vite';

function greetPlugin() {
  return {
    name: 'greet-plugin',
    resolveId(id) {
      if (id.endsWith('.greet')) return id;
    },
    load(id) {
      if (id.endsWith('.greet')) {
        return `export default "hi from ${id}"`;
      }
    },
  };
}

export default defineConfig({
  plugins: [greetPlugin()],
});

// Now your source can do: import greeting from './foo.greet'
// Vite resolves it, loads it as a JS module, browser gets standard JS.

External links

Exercise

Write a Vite plugin that transforms .md imports into a default-exported React component that renders the Markdown's HTML (use any md-to-html library). Then use it in a Vite + React app: import About from './about.md'; ...<About />.... The exercise shows the resolveId+load+transform pattern in 50 lines, and gives you a tool you'll actually use — markdown imports without the framework saying you can't.
Hint
In load, read the .md file with fs/promises. Convert with marked or remark. Wrap in: return import React from 'react'; export default () => React.createElement('div', { dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} } });``. The transformer's job is producing valid JS the browser can execute; the rest is React rendering it normally.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.