perf(shell): add fragment mode for fast rApp tab switching

When switching rApps from the dropdown, TabCache now requests
?fragment=1 which returns a lightweight JSON payload (~200 bytes)
instead of re-rendering the full 2000-line shell HTML template.
This eliminates server-side shell rendering and client-side
DOMParser overhead. Also prefetches fragments on hover.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-31 02:28:09 +00:00
parent 12499d62b5
commit f9457a0e11
4 changed files with 117 additions and 10 deletions

View File

@ -84,7 +84,7 @@ import { vnbModule } from "../modules/rvnb/mod";
import { crowdsurfModule } from "../modules/crowdsurf/mod";
import { spaces, createSpace, resolveCallerRole, roleAtLeast } from "./spaces";
import type { SpaceRoleString } from "./spaces";
import { renderShell, renderSubPageInfo, renderOnboarding } from "./shell";
import { renderShell, renderSubPageInfo, renderOnboarding, setFragmentMode } from "./shell";
import { renderOutputListPage } from "./output-list";
import { renderMainLanding, renderSpaceDashboard } from "./landing";
import { syncServer } from "./sync-instance";
@ -2595,6 +2595,22 @@ for (const mod of getAllModules()) {
return next();
});
}
// Fragment mode: signal renderShell to return lightweight JSON instead of full shell HTML.
// Used by TabCache for fast tab switching — skips the entire 2000-line shell template.
app.use(`/:space/${mod.id}`, async (c, next) => {
if (c.req.method !== "GET" || !c.req.query("fragment")) return next();
// Set the global flag so renderShell returns JSON fragment
setFragmentMode(true);
await next();
// renderShell already returned JSON — fix the content-type header
if (c.res.headers.get("content-type")?.includes("text/html")) {
const body = await c.res.text();
c.res = new Response(body, {
status: c.res.status,
headers: { "content-type": "application/json" },
});
}
});
app.route(`/:space/${mod.id}`, mod.routes);
// Auto-mount browsable output list pages
if (mod.outputPaths) {

View File

@ -85,6 +85,54 @@ export function getSpaceShellMeta(spaceSlug: string): { enabledModules: string[]
};
}
/**
* Render a lightweight JSON fragment for tab-cache.
* Returns only the body HTML, title, scripts, and styles no shell chrome.
* Used by TabCache's ?fragment=1 requests to avoid re-rendering the full 2000-line shell.
*/
export function renderFragment(opts: {
title: string;
body: string;
scripts?: string;
styles?: string;
}): string {
// Parse script srcs from the scripts string
const scriptSrcs: string[] = [];
const scriptRe = /src="([^"]+)"/g;
let m: RegExpExecArray | null;
if (opts.scripts) {
while ((m = scriptRe.exec(opts.scripts)) !== null) {
scriptSrcs.push(m[1]);
}
}
// Parse stylesheet hrefs from the styles string
const styleSrcs: string[] = [];
const styleRe = /href="([^"]+)"/g;
if (opts.styles) {
while ((m = styleRe.exec(opts.styles)) !== null) {
styleSrcs.push(m[1]);
}
}
// Extract inline <style> content
const inlineStyles: string[] = [];
const inlineRe = /<style[^>]*>([\s\S]*?)<\/style>/g;
if (opts.styles) {
while ((m = inlineRe.exec(opts.styles)) !== null) {
if (m[1].trim()) inlineStyles.push(m[1]);
}
}
return JSON.stringify({
title: opts.title,
body: opts.body,
scripts: scriptSrcs,
styles: styleSrcs,
inlineStyles,
});
}
export interface ShellOptions {
/** Page <title> */
title: string;
@ -112,6 +160,8 @@ export interface ShellOptions {
enabledModules?: string[] | null;
/** Whether this space has client-side encryption enabled */
spaceEncrypted?: boolean;
/** If true, return lightweight JSON fragment instead of full shell HTML */
fragment?: boolean;
/** Optional tab bar rendered below the subnav. */
tabs?: Array<{ id: string; label: string; icon?: string }>;
/** Active tab ID (matched from URL path by server). First tab if omitted. */
@ -138,7 +188,23 @@ export function buildSpaceUrl(space: string, path: string, host?: string): strin
return `http://${h}/${space}${path}`;
}
/** Global flag set by middleware to signal fragment mode to renderShell. */
let _fragmentMode = false;
export function setFragmentMode(on: boolean) { _fragmentMode = on; }
export function renderShell(opts: ShellOptions): string {
// Fragment mode: return lightweight JSON instead of full shell HTML.
// Triggered by middleware setting the flag, or by opts.fragment.
if (opts.fragment || _fragmentMode) {
_fragmentMode = false; // Reset after use
return renderFragment({
title: opts.title,
body: opts.body,
scripts: opts.scripts,
styles: opts.styles,
});
}
const {
title,
moduleId,

View File

@ -415,6 +415,20 @@ export class RStackAppSwitcher extends HTMLElement {
};
document.addEventListener("click", this.#outsideClickHandler);
// Prefetch module fragments on hover for faster tab switching.
// Uses low-priority fetch so it doesn't compete with user-initiated requests.
if ((window as any).__rspaceTabBar) {
this.#shadow.querySelectorAll("a.item").forEach((el) => {
el.addEventListener("mouseenter", () => {
const moduleId = (el as HTMLElement).dataset.id;
if (!moduleId || moduleId === current) return;
const space = this.#getSpaceSlug();
const href = `/${space}/${moduleId}?fragment=1`;
fetch(href, { priority: "low" } as RequestInit).catch(() => {});
}, { once: true });
});
}
// Intercept same-origin module links → dispatch event for tab system
// Only intercept when the shell tab system is active (window.__rspaceTabBar).
// On landing pages (rspace.online/, rspace.online/{moduleId}), let links

View File

@ -185,7 +185,6 @@ export class TabCache {
try {
const resolved = new URL(url, window.location.href);
if (resolved.origin !== window.location.origin) {
// Rewrite subdomain URL to path format: /{space}/{moduleId}
fetchUrl = `/${space}/${moduleId}`;
}
} catch {
@ -196,7 +195,9 @@ export class TabCache {
const app = document.getElementById("app");
if (!app) { console.warn("[TabCache] fetchAndInject: no #app"); return false; }
console.log("[TabCache] fetchAndInject:", fetchUrl, "for", moduleId);
// Append ?fragment=1 for lightweight JSON response (no full shell re-render)
const fragmentUrl = fetchUrl + (fetchUrl.includes("?") ? "&" : "?") + "fragment=1";
console.log("[TabCache] fetchAndInject:", fragmentUrl, "for", moduleId);
// Show loading spinner
const loadingPane = document.createElement("div");
@ -212,23 +213,33 @@ export class TabCache {
const timeoutId = setTimeout(() => controller.abort(), 15_000);
try {
const resp = await fetch(fetchUrl, {
headers: { "Accept": "text/html" },
const resp = await fetch(fragmentUrl, {
signal: controller.signal,
});
if (!resp.ok) {
clearTimeout(timeoutId);
console.warn("[TabCache] fetchAndInject: HTTP", resp.status, "for", fetchUrl);
console.warn("[TabCache] fetchAndInject: HTTP", resp.status, "for", fragmentUrl);
loadingPane.remove();
return false;
}
const html = await resp.text();
clearTimeout(timeoutId);
console.log("[TabCache] fetchAndInject: got", html.length, "bytes for", moduleId);
const content = this.extractContent(html);
const ct = resp.headers.get("content-type") || "";
let content: { body: string; title: string; scripts: string[]; styles: string[]; inlineStyles: string[] } | null;
if (ct.includes("application/json")) {
// Fast path: server returned pre-extracted JSON fragment
content = await resp.json();
console.log("[TabCache] fetchAndInject: got JSON fragment for", moduleId);
} else {
// Fallback: server returned full HTML (fragment middleware didn't match)
const html = await resp.text();
console.log("[TabCache] fetchAndInject: got", html.length, "bytes HTML (fallback) for", moduleId);
content = this.extractContent(html);
}
if (!content) {
console.warn("[TabCache] fetchAndInject: extractContent returned null for", moduleId, "— HTML has #app:", html.includes('id="app"'));
console.warn("[TabCache] fetchAndInject: no content extracted for", moduleId);
loadingPane.remove();
return false;
}