openvibers/openvibe.live Commits

Route bundles: the broadcast desk and Arena load when you go there, not on every page

OP OpenVibers committed 16 Sept 2026 · 25671e5470 · 3 files, +80 −8

Chrome's precise coverage on a loaded home page, nothing clicked: 14.3% of the JavaScript this
page shipped actually executed. The two clearest offenders were the broadcast desk (broadcast.js
plus broadcast-workspace.js, 471KB of source, 9% and 4% used) and the Arena (57KB, 4% used) —
parsed and compiled on every visit to every other page.

They now load when their route is opened, and again during idle time after first paint so a later
navigation is still instant. Taking them off the initial parse is the win; making someone wait for
a download when they finally click "Go live" would just move the cost somewhere more annoying.

Measured on the same page, medians of four cold loads, sampled two seconds in:

  JS parsed        2679KB -> 2145KB   (-20%)
  main-thread task  6.213s -> 5.448s   (-12.3%)
  script tags in the document  49 -> 46
  off the initial document                120.1 KB gzipped

Home-page coverage moves 14.3% -> 16.3% used, with unused bytes down from 2165KB to 1673KB.

Two things made this safe rather than a rewrite. Everything outside those files that reaches into
them already guards with `typeof fn === 'function'` — chat.js checks broadcastState, guides-live.js
checks the workspace helpers — so a page where the bundle has not loaded behaves as it did before
that route existed. And execution order inside a bundle is preserved (async=false on an injected
script keeps document order), which matters because broadcast-workspace.js has to run before
broadcast.js, as the old tag order did.

broadcast.js booted from a DOMContentLoaded listener, which never fires for a script that loads
after the document is parsed. It now runs immediately when the document is already complete and
waits only when it genuinely is not — the same pattern home-fx.js and footer-live.js already use.
routeFromURL falls back to a resolved promise if the inline loader is missing, so a stale cached
document still routes.

Verified end to end: on the home page neither bundle is requested and loadBroadcastPage is
undefined; navigating to /broadcast loads both in order, defines loadBroadcastPage and
_wsLoadManagedStreams, activates the page and runs the boot; /arena likewise. No console errors.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/public/index.html b/public/index.html
iindex 2125cf84..d6798f8c 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -5117,7 +5117,50 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/ov-skeleton.js?v=1" defer></script>
<script src="/js/ov-odometer.js?v=3" defer></script>
<script src="/js/ov-density.js?v=3" defer></script>
--<script src="/js/app.js?v=175" defer></script>
++<!-- ── Route bundles ──────────────────────────────────────────────────────────
++ Measured with Chrome's precise coverage on a loaded home page: 14.3% of the JavaScript this
++ page ships actually executes. The broadcast desk and the Arena are the clearest of the rest —
++ 471KB and 57KB of source that only ever runs on /broadcast and /arena, parsed and compiled on
++ every visit to every other page.
++
++ They load when the route is opened, and again during idle time after first paint so a later
++ navigation is instant. Execution order inside a bundle is preserved (async=false on an
++ injected script keeps document order), which matters: broadcast-workspace.js has to run
++ before broadcast.js, as the old tag order did.
++
++ Everything outside these files that reaches into them already guards with
++ `typeof fn === 'function'`, so a page where the bundle has not loaded behaves as it did
++ before the route existed rather than throwing. -->
++<script>
++(function () {
++ var BUNDLES = {
++ broadcast: ['/js/broadcast-workspace.js?v=16', '/js/broadcast.js?v=31'],
++ arena: ['/js/arena.js?v=18']
++ };
++ var started = Object.create(null);
++ function one(src) {
++ if (started[src]) return started[src];
++ started[src] = new Promise(function (resolve) {
++ var s = document.createElement('script');
++ s.src = src;
++ s.async = false; // keeps execution in the order they are appended
++ s.onload = function () { resolve(true); };
++ s.onerror = function () { console.warn('[routes] failed to load', src); resolve(false); };
++ document.head.appendChild(s);
++ });
++ return started[src];
++ }
++ window.ovLoadRoute = function (name) {
++ var list = BUNDLES[name];
++ if (!list) return Promise.resolve();
++ return Promise.all(list.map(one)); // appended together, so they run in order
++ };
++ window.ovPrefetchRoutes = function () {
++ for (var k in BUNDLES) window.ovLoadRoute(k);
++ };
++})();
++</script>
++<script src="/js/app.js?v=176" defer></script>
<script src="/js/stream-player.js?v=22" defer></script>
<script src="/js/stream-pip.js?v=1" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
@@@ -5181,15 +5224,12 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/broadcast-media-setup.js?v=1" defer></script>
<script src="/js/broadcast-devices.js?v=2" defer></script>
<script src="/js/broadcast-vods.js?v=2" defer></script>
--<script src="/js/broadcast-workspace.js?v=16" defer></script>
--<script src="/js/broadcast.js?v=30" defer></script>
<script src="/js/call.js?v=3" defer></script>
<script src="/js/voice-channels.js?v=2" defer></script>
<script src="/js/settings.js?v=12" defer></script>
<script src="/js/staff-console.js?v=1" defer></script>
<script src="/js/dashboard-moderation.js?v=5" defer></script>
<script src="/js/ai-viewers.js?v=3" defer></script>
--<script src="/js/arena.js?v=18" defer></script>
<script src="/js/recap.js?v=1" defer></script>
<script src="/js/context-cards.js?v=4" defer></script>
<script src="/js/powerchat.js?v=10" defer></script>
ddiff --git a/public/js/app.js b/public/js/app.js
iindex 341edaff..e99e566f 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -1145,6 +1145,32 @@ function setPageTitle(name) {
}
window.setPageTitle = setPageTitle;
++/** No-op fallback: if the inline bundle loader is missing, routes still resolve. */
++if (typeof window.ovLoadRoute !== 'function') window.ovLoadRoute = () => Promise.resolve();
++
++/**
++ * Warm the route bundles once the page has gone quiet.
++ *
++ * Taking them off the initial parse is the win; making the reader wait for a download when they
++ * finally click "Go live" would just move the cost somewhere more annoying. requestIdleCallback
++ * fetches them while nothing else is happening, so the first navigation to those routes is as
++ * instant as it was when they shipped on every page — without costing anything on the pages that
++ * never use them.
++ */
++(function warmRouteBundles() {
++ let done = false;
++ const go = () => {
++ if (done) return; done = true;
++ try { if (typeof window.ovPrefetchRoutes === 'function') window.ovPrefetchRoutes(); } catch { /* */ }
++ };
++ const start = () => {
++ if ('requestIdleCallback' in window) requestIdleCallback(go, { timeout: 12000 });
++ else setTimeout(go, 6000);
++ };
++ if (document.readyState === 'complete') setTimeout(start, 2500);
++ else window.addEventListener('load', () => setTimeout(start, 2500), { once: true });
++})();
++
function routeFromURL() {
// Remove the server-rendered SEO prerender block once the SPA takes over (it's crawlable
// content for no-JS scrapers; JS clients render the real interactive page instead).
@@@ -1193,8 +1219,9 @@ function routeFromURL() {
navigate('/dashboard', true);
return;
} else if (segments[0] === 'broadcast') {
++ // Show the shell first so the route feels instant, then fill it once its bundle is in.
showPage('broadcast');
-- loadBroadcastPage();
++ ovLoadRoute('broadcast').then(() => { if (typeof loadBroadcastPage === 'function') loadBroadcastPage(); });
} else if (segments[0] === 'admin') {
window.location.href = `${getOpenVibeToolsUrl()}/admin`;
return;
@@@ -1233,7 +1260,7 @@ function routeFromURL() {
if (typeof loadRecapPage === 'function') loadRecapPage(segments[1]);
} else if (segments[0] === 'arena') {
showPage('arena');
-- if (typeof loadArenaPage === 'function') loadArenaPage(segments);
++ ovLoadRoute('arena').then(() => { if (typeof loadArenaPage === 'function') loadArenaPage(segments); });
} else if (segments[0] === 'updates') {
showPage('updates');
loadUpdatesPage();
ddiff --git a/public/js/broadcast.js b/public/js/broadcast.js
iindex e5846927..b96e1706 100644
---- a/public/js/broadcast.js
++++ b/public/js/broadcast.js
@@@ -5754,7 +5754,10 @@ function _initPipDragResize() {
preview.addEventListener('pointercancel', onPointerUp);
}
--document.addEventListener('DOMContentLoaded', () => {
++// This file is loaded on demand when /broadcast is opened, which is almost always after
++// DOMContentLoaded has already fired — a listener added then never runs. Run immediately if the
++// document is already parsed, and wait only when it genuinely is not.
++const _bcBoot = () => {
initBroadcastSettingsListeners();
// Restoring the saved tab can issue broadcast API calls and force a layout on a hidden page.
// Only do it once the broadcast desk is the route the visitor is actually on.
@@@ -5829,7 +5832,9 @@ document.addEventListener('DOMContentLoaded', () => {
// ── PiP drag/resize on preview ──
_initPipDragResize();
--});
++};
++if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', _bcBoot);
++else _bcBoot();
// ═══════════════════════════════════════════════════════════════
// Media Request PiP Player (inline on broadcast page)