perf(webui): defer page-specific scripts to first navigation (Phase B)

7 page-specific controllers were eager-loaded in index.html on every cold
visit, even when the user lands on /dashboard and never opens /backup,
/admin, etc. Moved them to lazy-load via spa.js's existing loadScript()
helper, fired from each route's handler on first navigation:

  /js/components/backup/backup-page.js       — handleBackup()
  /js/components/backup/backup-app-card.js   — handleBackup()
  /js/components/ssh/ssh-page.js             — config-manager ssh-access
  /js/components/peers/peers-page.js         — config-manager peers
  /js/components/admin/admin-overview.js     — config-manager overview
  /js/components/admin/charts.js             — config-manager overview
  /js/components/admin/admin-system.js       — config-manager system

config-manager.js gets a tiny `lazyLoad` helper that delegates to
window.spaClean.loadScript with a graceful fallback when the SPA hasn't
booted (legacy paths). loadScript is idempotent — subsequent visits to
the same route are no-ops, so we don't re-fetch after the first nav.

Cold-load impact on /dashboard (the most common landing):
  Before: 25 sync <script> tags loading ~1.7 MB raw / ~430 KB gzipped
  After:  18 sync <script> tags loading ~1.5 MB raw / ~380 KB gzipped
  + corresponding parse-cost reduction on the client (no longer parsing
    backup-page.js + apps-related JS just to render the dashboard)

Page-specific JS still loads cleanly when the user navigates there — a
single extra network round-trip per route on first visit, then cached
for 1h (per Phase A's cache headers). Compression (Phase A) means the
deferred JS is ~75 % smaller on the wire than it would have been
pre-Phase-A.

Sister update to .../Scripts/update.sh: rsync now uses --delete so
file removals in the source tree (this commit deletes 7 script tags;
earlier commits deleted config-manager-old.js) propagate to the live
install. Excludes still protect frontend/data/.

Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-26 22:25:36 +01:00
parent 1cead7fc89
commit d123eda869
3 changed files with 29 additions and 7 deletions

View File

@ -97,13 +97,14 @@
<script src="/js/system/setup-wizard.js"></script>
<script src="/js/system/setup-completion-watcher.js"></script>
<script src="/js/system/system-orchestrator.js"></script>
<script src="/js/components/backup/backup-page.js"></script>
<script src="/js/components/backup/backup-app-card.js"></script>
<script src="/js/components/ssh/ssh-page.js"></script>
<script src="/js/components/peers/peers-page.js"></script>
<script src="/js/components/admin/charts.js"></script>
<script src="/js/components/admin/admin-overview.js"></script>
<script src="/js/components/admin/admin-system.js"></script>
<!--
Page-specific controllers are loaded on demand by spa.js / config-manager.js
when the user navigates to the relevant route. Keeping them out of the
initial <script> block trims ~200 KB raw (~50 KB gzipped) off the cold-load
cost AND avoids parsing them up front on the dashboard, which most users
land on. Each handler's loadScript() call is idempotent — subsequent
navigations to the same route are free.
-->
<script src="/js/spa.js"></script>
</body>
</html>

View File

@ -32,9 +32,21 @@ if (typeof window.ConfigManager === 'undefined') {
// the first call, so the config-category path below is a cache hit.
try { await this.core.loadConfig(category); } catch (e) {}
// Tool controllers are loaded on demand — they're not in index.html's
// initial <script> block (Phase B of the WebUI lazy-load work). Falls
// back gracefully if window.spaClean isn't around for some reason
// (e.g. legacy bootstrap path).
const lazyLoad = (src) =>
window.spaClean?.loadScript ? window.spaClean.loadScript(src) : Promise.resolve();
// Overview is the Admin landing — an ops/health board, not a config form.
if (category === 'overview') {
try { this.sidebar.populateSidebar(); } catch (e) {}
// charts.js is the chart-rendering helper admin-overview pulls in.
await Promise.all([
lazyLoad('/js/components/admin/admin-overview.js'),
lazyLoad('/js/components/admin/charts.js')
]);
if (typeof AdminOverview !== 'undefined') {
window.adminOverview = new AdminOverview('config-section');
await window.adminOverview.init();
@ -48,6 +60,7 @@ if (typeof window.ConfigManager === 'undefined') {
// a config category — render its own controller into the main pane.
if (category === 'ssh-access') {
try { this.sidebar.populateSidebar(); } catch (e) {}
await lazyLoad('/js/components/ssh/ssh-page.js');
if (typeof SshPage !== 'undefined') {
window.sshPage = new SshPage('config-section');
await window.sshPage.init();
@ -63,6 +76,7 @@ if (typeof window.ConfigManager === 'undefined') {
// we inject its content template, then init PeersPage.
if (category === 'peers') {
try { this.sidebar.populateSidebar(); } catch (e) {}
await lazyLoad('/js/components/peers/peers-page.js');
try {
const html = await fetch('/html/peers-content.html').then(r => r.text());
configSection.innerHTML = html;
@ -83,6 +97,7 @@ if (typeof window.ConfigManager === 'undefined') {
// own controller, like SSH Access above.
if (category === 'system') {
try { this.sidebar.populateSidebar(); } catch (e) {}
await lazyLoad('/js/components/admin/admin-system.js');
if (typeof AdminSystem !== 'undefined') {
window.adminSystem = new AdminSystem('config-section');
await window.adminSystem.init();

View File

@ -260,6 +260,12 @@ class LibrePortalSPAClean {
async handleBackup() {
try {
// backup-page.js + backup-app-card.js are loaded on first navigation.
// loadScript is idempotent — subsequent /backup navigations are no-ops.
await Promise.all([
this.loadScript('/js/components/backup/backup-page.js'),
this.loadScript('/js/components/backup/backup-app-card.js')
]);
const html = await this.fetchContent('/html/backup-content.html');
this.loadContent(html, 'Backups');
if (typeof BackupPage !== 'undefined') {