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

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:

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. This factory function constructs a platform-agnostic request handler used by Node, Bun, and Deno adapters.

// 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, the handler reads 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:

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. This utility re-renders the JSX module and packages the necessary assets for client-side injection.

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, orchestrates the update distribution:

// 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. Its entry point, hotReplace(href), establishes the SSE connection and manages DOM updates.

Opening the EventSource

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

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →