Live Reload
bext supports PHP-like live reload: change a source file and the browser updates automatically. No restart, no manual refresh. The server watches for file changes, triggers an optimized rebuild, atomically swaps the V8 pool, and notifies connected browsers to soft-refresh.
How it works#
Developer saves a file
│
▼
File watcher detects mtime change (1s poll)
│
▼
Debounce (200ms) — wait for batch saves
│
▼
Fast or full rebuild (see below)
│
▼
Create new V8 pool from fresh bundle
│
▼
Atomic swap (RwLock) — in-flight requests finish on old pool
│
▼
Clear ISR + component + compression caches
│
▼
Notify browsers (SSE event or ETag change)
│
▼
Browser soft-refreshes — swaps content + styles, no full reload
Configuration#
# bext.config.toml
[build]
watch_dirs = ["server"]
live_reload = true # enable in production (always on in dev mode)
debounce_ms = 200 # wait for batch saves (default: 200ms)
| Key | Type | Default | Description |
|---|---|---|---|
watch_dirs |
string[] |
[] |
Directories to watch for changes |
live_reload |
bool |
false |
Enable automatic reload in production |
debounce_ms |
u64 |
200 |
Debounce window for batch saves |
script |
string |
(none) | Build script to run (full rebuild only) |
bun_path |
string |
"bun" |
Path to the Bun binary |
timeout_secs |
u64 |
60 |
Maximum build time before timeout |
live_reload = true in bext.config.toml is required to enable the watcher in production. In
dev mode (bext run .) it is always on regardless of config.
Two rebuild modes#
The watcher automatically picks the best mode based on what changed:
| Changed files | Mode | Speed | What runs |
|---|---|---|---|
.ts, .tsx, .js, .jsx |
Fast | ~50ms | Direct bun build entry.ts — skips build script |
.css, .json, .toml |
Full | ~200ms | Runs your [build] script (Tailwind, etc.) |
The fast path invokes Bun's bundler directly with the entry file, bypassing the external build script. This means editing a page or route handler reloads in under 100ms total.
Client-side refresh#
When the server rebuilds, browsers need to know. The framework provides three notification modes:
Auto-detect (recommended)#
import { clientRuntime } from "@bext-stack/framework/client";
// In your template's <body>:
{clientRuntime({ liveReload: true })}
With liveReload: true, the client tries SSE first (instant, 0ms latency). If the server doesn't have the 2s latency). No configuration needed — it just works.realtime feature enabled, it falls back to ETag polling (
SSE mode (instant, requires realtime feature)#
{clientRuntime({ liveReload: "sse" })}
When the server swaps the pool, it pushes a "reload" event via Server-Sent Events. The browser receives it instantly and soft-refreshes. Requires [realtime] enabled = true in your bext config.
Polling mode (works everywhere)#
{clientRuntime({ liveReload: "poll", pollInterval: 2000 })}
The browser sends a HEAD request every 2 seconds and checks the ETag header. When the pool swaps, all caches are cleared, so the next request has a different ETag. The client detects the change and soft-refreshes.
No live reload (just client navigation)#
{clientRuntime({ navigation: true, liveReload: false })}
Client-side navigation still works (Turbo-like link interception, pushState, prefetch on hover) but no auto-refresh on server changes.
What "soft refresh" does#
When a rebuild is detected, the client doesn't do a full page reload. Instead:
- Re-fetches the current page's HTML via
fetch() - Updates
<link rel="stylesheet">tags — new cache-busted URLs are added, stale ones removed - Updates
<style>tags — inline styles are replaced with new content - Swaps
<main>content — the page body updates without touching<nav>,<header>,<footer> - Uses View Transitions API if available — smooth crossfade animation
This means:
- Navigation state is preserved (scroll position resets to top)
- External stylesheets with cache-busted URLs always pull the latest CSS
- Inline styles (common in template sites) are always fresh
- No flash of unstyled content
Client-side navigation#
The client runtime also provides SPA-like navigation (enabled by default):
{clientRuntime({ navigation: true })}
Features:
- Link interception — same-origin
<a>clicks fetch HTML and swap<main>, no full page load history.pushState— URL updates without reload, back/forward works- Prefetch on hover — after 65ms hover, pre-fetches the target page
- View Transitions API — smooth crossfade when available
- Fallback — external links, downloads, modifier keys (Ctrl+click) still work normally
Combined with live reload:
{clientRuntime({ navigation: true, liveReload: true })}
This gives you instant page transitions AND automatic updates when you edit source files.
Cache-busted assets#
For Tailwind or other external CSS, use content-hashed filenames:
// Build script
import { buildCSS } from "@bext-stack/framework/tailwind";
await buildCSS({
sources: ["server"],
outFile: "dist/styles.[hash].css", // → styles.18vjj5s.css
manifest: "dist/css-manifest.json", // → { "styles.*.css": "styles.18vjj5s.css" }
});
// Template
import { loadManifest, asset, stylesheet } from "@bext-stack/framework";
loadManifest("dist/css-manifest.json");
function RootLayout({ children }) {
return (
<html>
<head>
{stylesheet(asset("/styles.css"))} {/* → /styles.18vjj5s.css */}
</head>
<body>
<main>{children}</main>
{clientRuntime({ liveReload: true })}
</body>
</html>
);
}
When the CSS changes, the build produces a new hash, the manifest updates, and the template references the new URL. The client's soft-refresh detects the changed <link> tag and loads the new stylesheet.
Three trigger methods#
1. Automatic (file watcher)#
Enabled by watch_dirs + live_reload = true. Watches .ts, .tsx, .js, .jsx, .css, .json, .toml files. Ignores node_modules, .git, dist.
2. API endpoint#
# Full rebuild (runs build script)
curl -X POST http://localhost:3000/__bext/admin/api/bundle/reload
# Fast rebuild (TS only, skips build script)
curl -X POST http://localhost:3000/__bext/admin/api/bundle/fast-reload
Response:
{
"ok": true,
"mode": "fast",
"bundle_kb": 44,
"build_ms": 48,
"load_ms": 12,
"total_ms": 60
}
3. SIGHUP signal#
kill -HUP $(pidof bext-server)
Useful for deploy scripts and systemd integration.
Production safety#
Live reload is safe for production:
- Zero dropped requests.
RwLockallows concurrent reads (every request) with rare writes (pool swap). The write lock blocks new requests for microseconds. - Failed builds are harmless. If the build fails, the old pool and caches remain untouched. An error is logged, the server keeps running.
- Bounded memory. The old pool is dropped after in-flight requests complete. No leak from repeated reloads.
- Correct caches. ISR, component, and compression caches are cleared atomically after the swap. No stale content.
Build times#
| Phase | Duration |
|---|---|
| File change detection | ~1s (polling interval) |
| Debounce window | 200ms (configurable) |
| Fast rebuild (TS only) | 30-80ms |
| Full rebuild (with Tailwind) | 100-300ms |
| V8 pool creation (4 workers) | 5-15ms |
| Atomic pool swap | < 1ms |
| Client notification (SSE) | < 1ms |
| Client notification (poll) | ~2s |
| Total (fast + SSE) | ~250ms |
| Total (full + poll) | ~3.5s |
Example: docs site with instant preview#
# bext.config.toml
[server]
port = 3021
[build]
watch_dirs = ["server", "content"]
live_reload = true
debounce_ms = 300
// server/entry.ts
import { createSite } from "@bext-stack/framework/entry";
import { defineRoutes } from "@bext-stack/framework/router";
import { clientRuntime } from "@bext-stack/framework/client";
import { stylesheet } from "@bext-stack/framework";
import { h } from "@bext-stack/framework";
import * as pages from "./pages";
const router = defineRoutes({
layout: ({ children }) => (
<html>
<head>
{stylesheet("/styles.css")}
</head>
<body>
<nav><a href="/">Home</a> | <a href="/docs">Docs</a></nav>
<main>{children}</main>
{clientRuntime({ navigation: true, liveReload: true })}
</body>
</html>
),
routes: {
"/": { page: () => <h1>Welcome</h1> },
"/docs/[...path]": {
page: (ctx) => {
const content = pages.getPage("/docs/" + ctx.params.path);
return content
? <article dangerouslySetInnerHTML={{ __html: content.html }} />
: <p>Doc not found.</p>;
},
},
},
});
createSite({ hostname: "docs.example.com", router });
Edit any file in server/ or content/ → the browser updates in under a second, with smooth transitions and no full page reload.
Related#
- Client Runtime — the
clientRuntime()function that drives the browser-side refresh - PRISM Framework — PRISM's per-route watcher (separate from template-site live reload)
- Template Framework —
createSite()andSiteConfiglive reload works inside - Tailwind CSS — the CSS build that triggers full-rebuild mode on
.cssfile changes - Configuration: Build Flags —
watch_dirs,debounce_ms, and all related config keys