Code-split shell.js: lazy-load Automerge/WASM offline chunk
CI/CD / deploy (push) Failing after 7s Details

Move RSpaceOfflineRuntime, CommunitySync, OfflineStore, and
RStackHistoryPanel into a new shell-offline.ts chunk loaded via
dynamic import(). This removes ~2.5MB of Automerge WASM from the
critical path, reducing blocking JS from ~960KB to ~150KB brotli.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-01 12:40:32 -07:00
parent 9fd3ca931c
commit 6018a88d26
3 changed files with 76 additions and 59 deletions

View File

@ -77,6 +77,7 @@ export default defineConfig({
rollupOptions: {
output: {
entryFileNames: "shell.js",
chunkFileNames: "assets/[name]-[hash].js",
},
},
},
@ -1416,7 +1417,8 @@ export default defineConfig({
// Compute 8-char SHA-256 content hashes for module + shell assets
const hashableFiles = allFiles.filter((f) =>
(f.startsWith("/modules/") && (f.endsWith(".js") || f.endsWith(".css")) && !f.includes("-demo.js")) ||
f === "/shell.js" || f === "/shell.css" || f === "/theme.css"
f === "/shell.js" || f === "/shell.css" || f === "/theme.css" ||
(f.startsWith("/assets/shell-") && f.endsWith(".js"))
);
const hashes: Record<string, string> = {};
for (const file of hashableFiles) {
@ -1441,7 +1443,8 @@ export default defineConfig({
f === "/shell.css" ||
f === "/theme.css" ||
f === "/favicon.png" ||
f === "/manifest.json"
f === "/manifest.json" ||
(f.startsWith("/assets/shell-") && f.endsWith(".js"))
).map((f) => hashes[f] ? `${f}?v=${hashes[f]}` : f);
// Ensure root URL is present
if (!core.some((f) => f === "/" || f.startsWith("/?v="))) core.unshift("/");

66
website/shell-offline.ts Normal file
View File

@ -0,0 +1,66 @@
/**
* Lazy-loaded offline runtime chunk.
*
* Contains Automerge + WASM (~2.5MB) split from shell.ts so the main
* shell bundle stays small and doesn't block first paint.
*/
import { RStackHistoryPanel } from "../shared/components/rstack-history-panel";
import { RSpaceOfflineRuntime } from "../shared/local-first/runtime";
import { CommunitySync } from "../lib/community-sync";
import { OfflineStore } from "../lib/offline-store";
// Define the history panel component (depends on Automerge)
RStackHistoryPanel.define();
export function initOffline(spaceSlug: string) {
const runtime = new RSpaceOfflineRuntime(spaceSlug);
(window as any).__rspaceOfflineRuntime = runtime;
// Configure module scope resolution from server-rendered data
try {
const scopeJson = document.body?.getAttribute("data-scope-overrides");
const overrides: Record<string, string> = scopeJson ? JSON.parse(scopeJson) : {};
const moduleList: Array<{ id: string; scoping?: { defaultScope: string } }> =
(window as any).__rspaceModuleList || [];
const scopes: Array<{ id: string; scope: 'global' | 'space' }> = moduleList.map(m => ({
id: m.id,
scope: (overrides[m.id] || m.scoping?.defaultScope || 'space') as 'global' | 'space',
}));
runtime.setModuleScopes(scopes);
} catch { /* scope config unavailable — defaults to space-scoped */ }
runtime.init().catch((e: unknown) => {
console.warn("[shell] Offline runtime init failed — REST fallback only:", e);
});
// Flush pending writes before the page unloads
window.addEventListener("beforeunload", () => {
runtime.flush();
});
// ── CommunitySync (tab list Automerge sync) ──
const offlineStore = new OfflineStore();
const communitySync = new CommunitySync(spaceSlug, offlineStore);
(window as any).__communitySync = communitySync;
(async () => {
try {
await offlineStore.open();
await communitySync.initFromCache();
} catch (e) {
console.warn("[shell] CommunitySync cache init failed:", e);
}
document.dispatchEvent(new CustomEvent("community-sync-ready", {
detail: { sync: communitySync, communitySlug: spaceSlug },
}));
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${proto}//${location.host}/ws/${spaceSlug}`;
communitySync.connect(wsUrl);
})();
window.addEventListener("beforeunload", () => {
communitySync.saveBeforeUnload();
});
}

View File

@ -15,7 +15,6 @@ import { RStackTabBar } from "../shared/components/rstack-tab-bar";
import { RStackMi } from "../shared/components/rstack-mi";
import { RStackModuleSetup } from "../shared/components/rstack-module-setup";
import { RStackHistoryPanel } from "../shared/components/rstack-history-panel";
import { RStackOfflineIndicator } from "../shared/components/rstack-offline-indicator";
import { RStackSharePanel } from "../shared/components/rstack-share-panel";
import { RStackCommentBell } from "../shared/components/rstack-comment-bell";
@ -23,9 +22,6 @@ import { RStackCollabOverlay } from "../shared/components/rstack-collab-overlay"
import { RStackUserDashboard } from "../shared/components/rstack-user-dashboard";
import { rspaceNavUrl } from "../shared/url-helpers";
import { TabCache } from "../shared/tab-cache";
import { RSpaceOfflineRuntime } from "../shared/local-first/runtime";
import { CommunitySync } from "../lib/community-sync";
import { OfflineStore } from "../lib/offline-store";
// Expose URL helper globally (used by shell inline scripts + components)
(window as any).__rspaceNavUrl = rspaceNavUrl;
@ -42,67 +38,19 @@ RStackTabBar.define();
RStackMi.define();
RStackModuleSetup.define();
RStackHistoryPanel.define();
RStackOfflineIndicator.define();
RStackSharePanel.define();
RStackCommentBell.define();
RStackCollabOverlay.define();
RStackUserDashboard.define();
// ── Offline Runtime ──
// Instantiate the shared runtime from the space slug on the <body> tag.
// Components access it via window.__rspaceOfflineRuntime.
// ── Offline Runtime (lazy-loaded) ──
// Automerge + WASM (~2.5MB) loaded in a separate chunk to avoid blocking first paint.
// Components that depend on window.__rspaceOfflineRuntime already handle late init.
const spaceSlug = document.body?.getAttribute("data-space-slug");
if (spaceSlug) {
const runtime = new RSpaceOfflineRuntime(spaceSlug);
(window as any).__rspaceOfflineRuntime = runtime;
// Configure module scope resolution from server-rendered data
try {
const scopeJson = document.body?.getAttribute("data-scope-overrides");
const overrides: Record<string, string> = scopeJson ? JSON.parse(scopeJson) : {};
// Build scope config: merge module defaults with space overrides
const moduleList: Array<{ id: string; scoping?: { defaultScope: string } }> =
(window as any).__rspaceModuleList || [];
const scopes: Array<{ id: string; scope: 'global' | 'space' }> = moduleList.map(m => ({
id: m.id,
scope: (overrides[m.id] || m.scoping?.defaultScope || 'space') as 'global' | 'space',
}));
runtime.setModuleScopes(scopes);
} catch { /* scope config unavailable — defaults to space-scoped */ }
runtime.init().catch((e: unknown) => {
console.warn("[shell] Offline runtime init failed — REST fallback only:", e);
});
// Flush pending writes before the page unloads
window.addEventListener("beforeunload", () => {
runtime.flush();
});
// ── CommunitySync (tab list Automerge sync) ──
const offlineStore = new OfflineStore();
const communitySync = new CommunitySync(spaceSlug, offlineStore);
(window as any).__communitySync = communitySync;
(async () => {
try {
await offlineStore.open();
await communitySync.initFromCache();
} catch (e) {
console.warn("[shell] CommunitySync cache init failed:", e);
}
document.dispatchEvent(new CustomEvent("community-sync-ready", {
detail: { sync: communitySync, communitySlug: spaceSlug },
}));
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${proto}//${location.host}/ws/${spaceSlug}`;
communitySync.connect(wsUrl);
})();
window.addEventListener("beforeunload", () => {
communitySync.saveBeforeUnload();
import('./shell-offline').then(m => m.initOffline(spaceSlug)).catch(e => {
console.warn("[shell] Failed to load offline chunk:", e);
});
}