# How SXO's Hot Reload Mechanism Works Internally: Server-Sent Events and DOM Swapping

> Explore SXO's hot reload: learn how Server-Sent Events update HTML and assets, swapping DOM content while keeping scroll position and state. Optimized for speed.

- Repository: [Víctor García/sxo](https://github.com/gc-victor/sxo)
- Tags: internals
- Published: 2026-03-02

---

**SXO's hot reload uses Server-Sent Events (SSE) to push updated HTML and assets from the dev server to the browser, which then swaps the `<body>` content and reloads scripts while preserving scroll position and component state.**

SXO is a lightweight static site generator that provides instant feedback during development through a sophisticated hot reload system. Unlike traditional WebSocket-based solutions, the framework leverages **Server-Sent Events (SSE)** to stream updates from the development server to the client. This article examines the internal architecture of SXO's hot reload mechanism, tracing the data flow from file watcher to DOM replacement using the actual source code from the `gc-victor/sxo` repository.

## The Three-Core Architecture of SXO Hot Reload

The hot reload pipeline consists of three integrated components working together to achieve seamless updates:

- **[`src/js/server/dev/core-handler.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/dev/core-handler.js)** – Creates the `/hot-replace` SSE endpoint and serves the client-side script
- **[`src/js/server/dev/core.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/dev/core.js)** – Builds the SSE payload, debounces file-watch events, and manages the routes manifest
- **[`src/js/server/hot-replace.client.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/hot-replace.client.js)** – Opens the `EventSource` connection, decodes payloads, and performs DOM replacement with state restoration

These modules form a closed loop where file changes trigger server-side recompilation, payload generation, and client-side DOM updates without requiring a full page refresh.

## Server-Side: Creating the SSE Endpoint and Handler

The development server initializes the hot reload system through `createDevHandler` in [`src/js/server/dev/core-handler.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/dev/core-handler.js). This factory function constructs a platform-agnostic request handler used by Node, Bun, and Deno adapters.

```javascript
// src/js/server/dev/node.js (and bun/deno adapters)
import { createDevHandler } from "./core-handler.js";

const handler = createDevHandler({
  getRoutes,
  loadJsxModule,
  publicPath,
  getEsbuildError,
  hotReplaceClientPath,
  readFile,
  getMiddleware,
  resolve404Page,
  resolve500Page,
  logger,
});

```

The handler exposes two critical endpoints for the hot reload workflow.

### Serving the Client Script

When the browser requests [`/hot-replace.js`](https://github.com/gc-victor/sxo/blob/main//hot-replace.js), the handler reads [`hot-replace.client.js`](https://github.com/gc-victor/sxo/blob/main/hot-replace.client.js) from disk and returns it with `Content-Type: application/javascript`. This script gets automatically injected before the closing `</head>` tag of every page during development.

### Establishing the SSE Connection

The `/hot-replace?href=<path>` endpoint handles SSE connections. When a request matches this pathname and includes an `href` query parameter, `handleSSE(request, href)` creates a `ReadableStream` whose controller is stored in an `SSEClient` object:

```javascript
const stream = new ReadableStream({
  async start(controller) {
    // Controller stored in SSEClient and pushed to sseClients array
  }
});
return new Response(stream, { status: 200, headers: sseHeaders });

```

The stream's `start` callback immediately transmits an initial payload containing the current page state.

## Building the Hot Reload Payload

The server constructs update payloads using `buildHotReplacePayload` in [`src/js/server/dev/core.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/dev/core.js). This utility re-renders the JSX module and packages the necessary assets for client-side injection.

```javascript
export async function buildHotReplacePayload({ route, params, jsxFn, publicPath }) {
  const html = await jsxFn(params);
  const body = extractBodyFromHtml(html);
  const normalized = normalizePublicPath(publicPath);
  return JSON.stringify({ 
    body, 
    assets: { 
      css: route.assets?.css || [], 
      js: route.assets?.js || [] 
    }, 
    publicPath: normalized 
  });
}

```

The payload contains three essential fields:

- **`body`** – The HTML content inside the `<body>` tag of the freshly rendered page
- **`assets`** – An object containing arrays of CSS and JavaScript paths for the route
- **`publicPath`** – The normalized `PUBLIC_PATH` setting (empty strings preserved)

The `extractBodyFromHtml` helper uses a RegExp to isolate body content, while `normalizePublicPath` strips trailing slashes but maintains empty strings to match documented behavior.

If **esbuild** encounters a compilation error, the handler generates a fallback payload containing only the error HTML rendered in the body.

## Triggering Reloads via File Watching

Platform-specific file watchers invoke `broadcastReload` whenever source files change. This method, attached to the handler object in [`core-handler.js`](https://github.com/gc-victor/sxo/blob/main/core-handler.js), orchestrates the update distribution:

```javascript
// Attached to handler object
handleRequest.broadcastReload = broadcastReload;

```

The `broadcastReload` function executes the following sequence:

1. Optionally runs `reloadModules` to clear the ES-module cache
2. Cleans up closed SSE clients from the `sseClients` array
3. Retrieves the latest esbuild error status
4. Iterates over active clients, re-renders their associated routes via `loadJsxModule`, and builds fresh payloads
5. Enqueues payloads to each client's stream controller
6. Logs `"page::reloaded"` upon completion

If a client enqueue operation fails, the client is marked as closed and removed from the active pool.

## Client-Side: Processing Updates and DOM Replacement

The browser loads the hot-replace client script from [`/hot-replace.js`](https://github.com/gc-victor/sxo/blob/main//hot-replace.js). Its entry point, `hotReplace(href)`, establishes the SSE connection and manages DOM updates.

### Opening the EventSource

```javascript
evSource = new EventSource(`/hot-replace?href=${href}`);
evSource.onmessage = debounce(onHotMessage, 500);

```

The connection URL includes the current page path, allowing the server to route updates to the correct template.

### Processing Messages and DOM Swapping

The `onHotMessage` handler decodes the base64-encoded payload and executes a five-step replacement process:

1. **Capture state** – Records scroll positions via `captureScrollPositions` and serializes reactive component state using `preserveReactiveComponentsState`
2. **Replace content** – Swaps the `<body>` innerHTML with the new payload
3. **Load assets** – Injects new scripts and stylesheets with cache-busting timestamps via `scripts(data)` and `styles(data)`
4. **Restore scroll** – Reapplies saved scroll positions with `restoreScrollPositions`
5. **Restore state** – Schedules component state restoration via `scheduleStateRestoration`

### Handling Custom Elements and State Preservation

To avoid `DOMException: Cannot redefine custom element` errors, the client appends version suffixes to custom element tag names and swaps existing instances safely. Reactive components expose a `state` Map and `setState` method; the module snapshots these values into `window.___hrcStates` before replacement and restores them after new scripts execute.

Stylesheet updates append new `<link>` elements with cache-busting query parameters, wait for load events, then remove old links to prevent flash-of-unstyled-content.

## Summary

- **SXO hot reload** uses **Server-Sent Events** rather than WebSockets for unidirectional server-to-client communication
- The **`createDevHandler`** function in [`core-handler.js`](https://github.com/gc-victor/sxo/blob/main/core-handler.js) establishes the `/hot-replace` endpoint and serves the client script
- **Payload construction** happens in `buildHotReplacePayload`, which re-renders JSX and packages body HTML with asset manifests
- **File watchers** trigger `broadcastReload` to distribute updates to all connected SSE clients
- The **client script** performs surgical DOM replacement, preserving scroll position and reactive component state while avoiding custom element redefinition errors

## Frequently Asked Questions

### What protocol does SXO use for hot reload communication?

SXO uses **Server-Sent Events (SSE)** via the `EventSource` API. This unidirectional protocol allows the server to push updates to the browser over a persistent HTTP connection without the overhead of WebSocket handshakes. The implementation in [`src/js/server/dev/core-handler.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/dev/core-handler.js) creates a `ReadableStream` that remains open during the development session.

### How does SXO preserve component state during hot reloads?

The client script captures reactive component state before DOM replacement by accessing each element's `state` Map and storing values in `window.___hrcStates`. After injecting new scripts and replacing the body content, `scheduleStateRestoration` reapplies these values through components' `setState` methods. Scroll positions are similarly captured as coordinate arrays and restored via `element.scrollTo` calls.

### What happens when esbuild encounters a compilation error during development?

When the file watcher triggers `broadcastReload`, the handler checks `getEsbuildError` for compilation errors. If errors exist, `buildHotReplacePayload` generates a fallback payload containing only the error HTML in the body field. The client receives this payload and replaces the page content with the error display, allowing developers to see compilation failures immediately without breaking the SSE connection.

### Why does SXO use cache-busting timestamps for assets?

The client script in [`hot-replace.client.js`](https://github.com/gc-victor/sxo/blob/main/hot-replace.client.js) appends query parameters with timestamps to CSS and JavaScript URLs before injecting them into the DOM. This prevents the browser from serving cached versions of modified files, ensuring that hot reloads reflect the latest code changes immediately after esbuild completes recompilation.