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:
src/js/server/dev/core-handler.js– Creates the/hot-replaceSSE endpoint and serves the client-side scriptsrc/js/server/dev/core.js– Builds the SSE payload, debounces file-watch events, and manages the routes manifestsrc/js/server/hot-replace.client.js– Opens theEventSourceconnection, 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. 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 pageassets– An object containing arrays of CSS and JavaScript paths for the routepublicPath– The normalizedPUBLIC_PATHsetting (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:
- Optionally runs
reloadModulesto clear the ES-module cache - Cleans up closed SSE clients from the
sseClientsarray - Retrieves the latest esbuild error status
- Iterates over active clients, re-renders their associated routes via
loadJsxModule, and builds fresh payloads - Enqueues payloads to each client's stream controller
- 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:
- Capture state – Records scroll positions via
captureScrollPositionsand serializes reactive component state usingpreserveReactiveComponentsState - Replace content – Swaps the
<body>innerHTML with the new payload - Load assets – Injects new scripts and stylesheets with cache-busting timestamps via
scripts(data)andstyles(data) - Restore scroll – Reapplies saved scroll positions with
restoreScrollPositions - 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
createDevHandlerfunction incore-handler.jsestablishes the/hot-replaceendpoint and serves the client script - Payload construction happens in
buildHotReplacePayload, which re-renders JSX and packages body HTML with asset manifests - File watchers trigger
broadcastReloadto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →