HMR & Dev Server
Deep dive into Lunx's sub-18ms Hot Module Replacement engine, the uWebSockets.js server architecture, and rust-notify file watching for maximum development speed.
How HMR works in Lunx
Lunx's HMR pipeline is built from the ground up in Rust and operates in three distinct phases after a file change is detected:
- Detection — rust-notify detects the file change in ~6ms (vs chokidar's ~30ms)
- Recompilation — only the changed module is recompiled via SWC (~8ms for typical files)
- Push — the compiled module delta is pushed to the browser over a persistent WebSocket (~4ms)
# HMR log output when editing a component: [lunx] file changed: src/components/Button.tsx (6ms detect) [hmr] recompiling module... (8ms via SWC) [hmr] pushed delta to 1 client (4ms via WebSocket) [hmr] total: 18ms — state preserved ✓
uWebSockets.js server
Lunx replaces the Express-based dev server used by most tools with uWebSockets.js — a native C++ HTTP/WebSocket library with Node.js bindings. This provides dramatically lower memory usage and higher request throughput for asset serving.
| Metric | Vite (Express) | Lunx (uWS) | Improvement |
|---|---|---|---|
| Server cold boot | ~200ms | ~36ms | 5.6× faster |
| HMR p50 latency | ~80ms | ~12ms | 6.7× faster |
| HMR p99 latency | ~350ms | ~18ms | 19.4× faster |
| Memory (idle) | ~120MB | ~38MB | 3.2× faster |
| Concurrent WS conns | ~800 | ~10,000 | 12.5× faster |
Configuring the dev server
import { defineConfig } from 'lunx' export default defineConfig({ framework: 'react', server: { port: 3000, host: '0.0.0.0', // expose to LAN open: true, // launch browser on start https: true, // auto self-signed cert // Custom response headers headers: { 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp', }, // API proxy to backend proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, ''), }, '/ws': { target: 'ws://localhost:8080', ws: true, // enable WebSocket proxying }, }, }, })
HMR boundary behavior
Lunx propagates module updates up the component tree until it finds a component that can accept the hot update. If no boundary is found, a full page reload is triggered automatically.
// Explicitly accept hot updates in a module if (import.meta.hot) { import.meta.hot.accept((newModule) => { // Handle the updated module here console.log('Module updated:', newModule) }) // Clean up resources before a module is replaced import.meta.hot.dispose((data) => { data.cleanupFn = myCleanup }) }
Custom HMR events
Plugins can emit custom HMR events to the client, enabling advanced patterns like route-level invalidation or store resets without a full reload.
// In a Lunx plugin (server side): export default function myPlugin() { return { name: 'my-plugin', handleHotUpdate({ file, server }) { if (file.endsWith('.data.ts')) { server.hot.send({ type: 'custom', event: 'data-store:reset', data: { file } }) return [] // prevent default module update } } } } // In the browser (client side): import.meta.hot?.on('data-store:reset', ({ file }) => { console.log('Data store reset triggered by:', file) store.reset() })