Code-split shell.js: lazy-load Automerge/WASM offline chunk
CI/CD / deploy (push) Failing after 7s
Details
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:
parent
9fd3ca931c
commit
6018a88d26
|
|
@ -77,6 +77,7 @@ export default defineConfig({
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: "shell.js",
|
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
|
// Compute 8-char SHA-256 content hashes for module + shell assets
|
||||||
const hashableFiles = allFiles.filter((f) =>
|
const hashableFiles = allFiles.filter((f) =>
|
||||||
(f.startsWith("/modules/") && (f.endsWith(".js") || f.endsWith(".css")) && !f.includes("-demo.js")) ||
|
(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> = {};
|
const hashes: Record<string, string> = {};
|
||||||
for (const file of hashableFiles) {
|
for (const file of hashableFiles) {
|
||||||
|
|
@ -1441,7 +1443,8 @@ export default defineConfig({
|
||||||
f === "/shell.css" ||
|
f === "/shell.css" ||
|
||||||
f === "/theme.css" ||
|
f === "/theme.css" ||
|
||||||
f === "/favicon.png" ||
|
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);
|
).map((f) => hashes[f] ? `${f}?v=${hashes[f]}` : f);
|
||||||
// Ensure root URL is present
|
// Ensure root URL is present
|
||||||
if (!core.some((f) => f === "/" || f.startsWith("/?v="))) core.unshift("/");
|
if (!core.some((f) => f === "/" || f.startsWith("/?v="))) core.unshift("/");
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,6 @@ import { RStackTabBar } from "../shared/components/rstack-tab-bar";
|
||||||
import { RStackMi } from "../shared/components/rstack-mi";
|
import { RStackMi } from "../shared/components/rstack-mi";
|
||||||
|
|
||||||
import { RStackModuleSetup } from "../shared/components/rstack-module-setup";
|
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 { RStackOfflineIndicator } from "../shared/components/rstack-offline-indicator";
|
||||||
import { RStackSharePanel } from "../shared/components/rstack-share-panel";
|
import { RStackSharePanel } from "../shared/components/rstack-share-panel";
|
||||||
import { RStackCommentBell } from "../shared/components/rstack-comment-bell";
|
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 { RStackUserDashboard } from "../shared/components/rstack-user-dashboard";
|
||||||
import { rspaceNavUrl } from "../shared/url-helpers";
|
import { rspaceNavUrl } from "../shared/url-helpers";
|
||||||
import { TabCache } from "../shared/tab-cache";
|
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)
|
// Expose URL helper globally (used by shell inline scripts + components)
|
||||||
(window as any).__rspaceNavUrl = rspaceNavUrl;
|
(window as any).__rspaceNavUrl = rspaceNavUrl;
|
||||||
|
|
@ -42,67 +38,19 @@ RStackTabBar.define();
|
||||||
RStackMi.define();
|
RStackMi.define();
|
||||||
|
|
||||||
RStackModuleSetup.define();
|
RStackModuleSetup.define();
|
||||||
RStackHistoryPanel.define();
|
|
||||||
RStackOfflineIndicator.define();
|
RStackOfflineIndicator.define();
|
||||||
RStackSharePanel.define();
|
RStackSharePanel.define();
|
||||||
RStackCommentBell.define();
|
RStackCommentBell.define();
|
||||||
RStackCollabOverlay.define();
|
RStackCollabOverlay.define();
|
||||||
RStackUserDashboard.define();
|
RStackUserDashboard.define();
|
||||||
|
|
||||||
// ── Offline Runtime ──
|
// ── Offline Runtime (lazy-loaded) ──
|
||||||
// Instantiate the shared runtime from the space slug on the <body> tag.
|
// Automerge + WASM (~2.5MB) loaded in a separate chunk to avoid blocking first paint.
|
||||||
// Components access it via window.__rspaceOfflineRuntime.
|
// Components that depend on window.__rspaceOfflineRuntime already handle late init.
|
||||||
const spaceSlug = document.body?.getAttribute("data-space-slug");
|
const spaceSlug = document.body?.getAttribute("data-space-slug");
|
||||||
if (spaceSlug) {
|
if (spaceSlug) {
|
||||||
const runtime = new RSpaceOfflineRuntime(spaceSlug);
|
import('./shell-offline').then(m => m.initOffline(spaceSlug)).catch(e => {
|
||||||
(window as any).__rspaceOfflineRuntime = runtime;
|
console.warn("[shell] Failed to load offline chunk:", e);
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue