From d501aeb4eb2d9ec8aa2654d581593ca1480c88c2 Mon Sep 17 00:00:00 2001
From: l5y <220195275+l5yth@users.noreply.github.com>
Date: Tue, 28 Jul 2026 09:15:14 +0200
Subject: [PATCH] web: frontent performance fixes (#864)
* web: de-jank cold-load backfill + scope module preload to the view
* web: defer Leaflet, lazy-load node overlay, skip data pipeline off-dashboard
* web: address review of frontend-perf fix
---
ACCEPTANCE.md | 99 +++++++
.../application/helpers/asset_helpers.rb | 137 ++++++++-
.../main-collection-backfill.test.js | 137 +++++++++
.../__tests__/main-node-overlay-lazy.test.js | 173 +++++++++++
.../js/app/__tests__/main-view-gating.test.js | 106 +++++++
web/public/assets/js/app/main.js | 278 +++++++++++++++---
web/spec/app_spec.rb | 45 +++
web/spec/asset_import_map_spec.rb | 142 +++++++++
web/spec/helpers/asset_helpers_spec.rb | 47 +++
web/views/layouts/app.erb | 20 +-
10 files changed, 1135 insertions(+), 49 deletions(-)
create mode 100644 web/public/assets/js/app/__tests__/main-node-overlay-lazy.test.js
create mode 100644 web/public/assets/js/app/__tests__/main-view-gating.test.js
diff --git a/ACCEPTANCE.md b/ACCEPTANCE.md
index 9ff23d6..c39c55c 100644
--- a/ACCEPTANCE.md
+++ b/ACCEPTANCE.md
@@ -4418,3 +4418,102 @@ bridge poll/handle suites (every node mock now serves the `%21` path), the
MB-A1 compacted-row suite, and the per-id node route specs (synthetic flag,
stale/fresh, since-filter, opt-out) — the fallback fires only on the
empty-result path, so none of their outcomes may change.
+
+---
+
+## Bugfix: Frontend load performance regression
+
+Since the module-graph preload (#815/#832) and the bulk-collection backfill (#835),
+amplified by the design/UX work (#855/#859/#860), the dashboard's cold-load cost
+grew. Two root causes plus three first-paint levers are addressed, all
+frontend/template only — no API/DB change; the C4/C7 window floors,
+`MAX_QUERY_LIMIT`, privacy, and the FC persistent cache are untouched, and the full
+7-/28-day history still backfills (only *when* it repaints changes, not *which*
+rows are reachable):
+
+- **(RC-A)** the layout preloaded *every* served JS module on every page even
+ though a page only runs the graph reachable from its own entries → scope the
+ modulepreload set to the current view's **static** import closure (the AV3 import
+ map still versions the whole graph).
+- **(RC-B)** the one-shot backfill repainted the entire node table + map once per
+ streamed page (dozens of `/api/positions` pages on a busy instance), on the main
+ thread → coalesce the per-page repaints onto a bounded idle callback.
+- **(FP-A3)** the render-blocking Leaflet CDN `
- <%# Preload the whole ES-module graph in parallel so the browser fetches it in
- one round trip instead of discovering it import-tier by import-tier; this
- removes the waterfall that delayed the first /api data paint. Same set the
- import map versions; emitted after it so module URLs resolve to the busted
- targets. A module absent here still loads normally on demand. %>
- <%= asset_modulepreload_tags %>
+ <%# Preload — in parallel, so the browser skips the import-tier waterfall that
+ delayed the first /api data paint — only the module graph THIS view loads,
+ not the whole served tree (a landing page must not download the node-detail
+ / charts / federation page graphs it never runs). The import map above still
+ versions the whole graph (AV3), so a later navigation stays cache-busted, and
+ a module absent from this scoped set still loads on demand. %>
+ <% preload_view_mode = (defined?(current_view_mode) ? current_view_mode : nil) %>
+ <%= asset_modulepreload_tags(asset_preload_entry_modules(preload_view_mode)) %>
<%# Cold-load data prefetch: an early async module fires the first-load API
requests in parallel with the module graph so data paints without waiting
for the whole bundle to boot. Runs only on cold loads (the cache module
@@ -106,10 +108,16 @@
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
+ <%# `defer` so this external script does not block first paint on the unpkg
+ round-trip. Deferred scripts run in document order after parsing but before
+ DOMContentLoaded, and the map init runs on DOMContentLoaded, so `window.L`
+ is ready by the time the app initialises (a missing L still degrades to the
+ "map unavailable" placeholder). %>
<% body_classes = ["dark"]