lunx.docs
DocsTutorialsHMR & Dev Server

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)
bash
# 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.

MetricVite (Express)Lunx (uWS)Improvement
Server cold boot~200ms~36ms5.6× faster
HMR p50 latency~80ms~12ms6.7× faster
HMR p99 latency~350ms~18ms19.4× faster
Memory (idle)~120MB~38MB3.2× faster
Concurrent WS conns~800~10,00012.5× faster

Configuring the dev server

typescriptlunx.config.ts
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.

typescript
// 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.

typescript
// 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()
})
rust-notify vs chokidar
Lunx uses rust-notify (a native Rust library compiled to a Node.js addon) for file system watching. It uses OS-native APIs (FSEvents on macOS, inotify on Linux, ReadDirectoryChangesW on Windows) for ~6ms detection latency vs chokidar's polling-based ~30ms.