From f9457a0e115b2364a274c5e3144df6786b1a686c Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 31 Mar 2026 02:28:09 +0000 Subject: [PATCH] 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) --- server/index.ts | 18 ++++++- server/shell.ts | 66 ++++++++++++++++++++++++ shared/components/rstack-app-switcher.ts | 14 +++++ shared/tab-cache.ts | 29 +++++++---- 4 files changed, 117 insertions(+), 10 deletions(-) diff --git a/server/index.ts b/server/index.ts index 9025233..86f092c 100644 --- a/server/index.ts +++ b/server/index.ts @@ -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) { diff --git a/server/shell.ts b/server/shell.ts index c2457dc..2de60d0 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -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