Dashboard graph: lazy-load Mermaid bundle and serve compressed assets #74

Open
opened 2026-05-11 04:48:11 +02:00 by jesse · 0 comments
Owner

Background

The dashboard Graph tab loads Mermaid 10.9.1 (crates/cognix-server/assets/mermaid.min.js, 3 335 717 bytes / 3.18 MiB raw) via a synchronous <script> tag on every dashboard page view, even though Graph is one of four tabs and most page loads never render a graph. Asset routes serve raw bytes with no Content-Encoding — Mermaid gzips to ~700 KB and brotli-11s to ~520 KB, so the wire payload is 4–6× larger than necessary.

Source: PR #73 multi-agent code review (Performance dimension, two CRITICAL findings)
File: crates/cognix-server/assets/index.html:74, crates/cognix-server/src/transport/ui/assets.rs
Branch: phase-8/dashboard-ui-pr4-graph (now merged target)

Failure mode

  • Every dashboard visitor downloads, parses, and executes the full 3.3 MB Mermaid bundle even when the Graph tab is never clicked.
  • The script tag has neither defer nor async. PR #73 added defer, which unblocks parsing — but the asset is still fetched eagerly on every load.
  • No compression middleware exists in crates/cognix-server/src/transport/ui/; grep -E 'gzip|brotli|deflate|compress|content-encoding' returns zero hits.
  • test_dashboard_bundle_size_excluding_mermaid asserts non-Mermaid assets stay under 200 KB. The assertion is technically accurate but operationally misleading: the actual payload users download is ~17× that, and future Mermaid upgrades or new vendor deps will not be caught by a budget that explicitly excludes the elephant.
  • Estimated cost on a mid-tier laptop: ~80–150 ms parse + compile, ~15–30 MB transient heap, plus the full 3.3 MB transfer. On a 3G connection this single asset adds 5–10 s to first paint.

Why deferred from PR #73

  • Lazy-loading requires a script-injection lifecycle (load promise singleton, ready signal, loading-state UX) that is architecturally distinct from the correctness and security fixes that constituted the PR #73 fix loop.
  • Compression is new transport infrastructure (tower-http compression layer or build-time pre-compressed .br/.gz blobs); it touches the asset table and route handler shape and warrants its own review.
  • Both changes need before/after measurement to validate the win, which the fix loop did not produce.

Proposed work

  • Replace the eager <script defer src="/ui/assets/mermaid.min.js"> in index.html with on-demand loading: graph.js injects the script (or uses dynamic import() after rewrap as an ES module) on the first call to renderGraph, gated by a memoized mermaidLoadPromise singleton. Subsequent Graph-tab clicks reuse the loaded engine.
  • Show a Loading graph engine… message while the bundle is in-flight on the first activation.
  • Pre-compress vendored assets at build time. Embed both encodings via include_bytes!("mermaid.min.js.br") and include_bytes!("mermaid.min.js.gz"), plus the raw bytes. Pick the variant matching the request's Accept-Encoding; emit Content-Encoding: br|gzip and Vary: Accept-Encoding. Apply the same to other text assets in the asset table.
  • Tighten test_dashboard_bundle_size_excluding_mermaid (or add a sibling) into a test_dashboard_total_wire_payload_budget that asserts the total brotli'd payload of all served assets stays under a documented ceiling (suggested: 1.2 MB). Document the ceiling in docs/features/phases/phase-8-dashboard.md.
  • Capture Lighthouse / WPT metrics before and after, attach to the PR description.
  • Update docs/features/phases/phase-8-dashboard.md with the new lazy-load contract and the trust assumption (Mermaid only loads on Graph-tab activation).

Acceptance

  • Cold dashboard load with no Graph-tab interaction transfers under 200 KB and does not request mermaid.min.js.
  • First Graph-tab click loads Mermaid once; subsequent clicks reuse the cached engine without a network round-trip.
  • Content-Encoding: br (or gzip fallback) is present on all asset responses when the request advertises support; uncompressed bytes still served when the client opts out.
  • Asset SHA256 / byte-size pin tests in crates/cognix-server/src/transport/ui/assets.rs continue to pass against the raw bytes (compression is transport-layer, not content-layer).
  • New test_dashboard_total_wire_payload_budget test fails if the brotli'd total grows past the documented ceiling.
  • All existing PR #73 tests in transport::ui and tests/integration/ui_graph_routes.rs pass without modification.
  • No new cargo deny / cargo audit findings introduced by the compression middleware.

References

  • PR: #73feat: add dashboard thought graph viewer
  • Code review finding: CRITICAL — eager 3.3 MB Mermaid load on every page view
  • Code review finding: CRITICAL — no asset compression middleware
  • Project rule: .claude/rules/coding-style.md — file/function size budgets (informs the wire-payload budget pattern)
## Background The dashboard Graph tab loads Mermaid 10.9.1 (`crates/cognix-server/assets/mermaid.min.js`, 3 335 717 bytes / 3.18 MiB raw) via a synchronous `<script>` tag on every dashboard page view, even though Graph is one of four tabs and most page loads never render a graph. Asset routes serve raw bytes with no `Content-Encoding` — Mermaid gzips to ~700 KB and brotli-11s to ~520 KB, so the wire payload is 4–6× larger than necessary. **Source:** PR #73 multi-agent code review (Performance dimension, two CRITICAL findings) **File:** `crates/cognix-server/assets/index.html:74`, `crates/cognix-server/src/transport/ui/assets.rs` **Branch:** `phase-8/dashboard-ui-pr4-graph` (now merged target) ## Failure mode - Every dashboard visitor downloads, parses, and executes the full 3.3 MB Mermaid bundle even when the Graph tab is never clicked. - The script tag has neither `defer` nor `async`. PR #73 added `defer`, which unblocks parsing — but the asset is still fetched eagerly on every load. - No compression middleware exists in `crates/cognix-server/src/transport/ui/`; `grep -E 'gzip|brotli|deflate|compress|content-encoding'` returns zero hits. - `test_dashboard_bundle_size_excluding_mermaid` asserts non-Mermaid assets stay under 200 KB. The assertion is technically accurate but operationally misleading: the actual payload users download is ~17× that, and future Mermaid upgrades or new vendor deps will not be caught by a budget that explicitly excludes the elephant. - Estimated cost on a mid-tier laptop: ~80–150 ms parse + compile, ~15–30 MB transient heap, plus the full 3.3 MB transfer. On a 3G connection this single asset adds 5–10 s to first paint. ## Why deferred from PR #73 - Lazy-loading requires a script-injection lifecycle (load promise singleton, ready signal, loading-state UX) that is architecturally distinct from the correctness and security fixes that constituted the PR #73 fix loop. - Compression is new transport infrastructure (tower-http compression layer or build-time pre-compressed `.br`/`.gz` blobs); it touches the asset table and route handler shape and warrants its own review. - Both changes need before/after measurement to validate the win, which the fix loop did not produce. ## Proposed work - [ ] Replace the eager `<script defer src="/ui/assets/mermaid.min.js">` in `index.html` with on-demand loading: `graph.js` injects the script (or uses dynamic `import()` after rewrap as an ES module) on the first call to `renderGraph`, gated by a memoized `mermaidLoadPromise` singleton. Subsequent Graph-tab clicks reuse the loaded engine. - [ ] Show a `Loading graph engine…` message while the bundle is in-flight on the first activation. - [ ] Pre-compress vendored assets at build time. Embed both encodings via `include_bytes!("mermaid.min.js.br")` and `include_bytes!("mermaid.min.js.gz")`, plus the raw bytes. Pick the variant matching the request's `Accept-Encoding`; emit `Content-Encoding: br|gzip` and `Vary: Accept-Encoding`. Apply the same to other text assets in the asset table. - [ ] Tighten `test_dashboard_bundle_size_excluding_mermaid` (or add a sibling) into a `test_dashboard_total_wire_payload_budget` that asserts the *total* brotli'd payload of all served assets stays under a documented ceiling (suggested: 1.2 MB). Document the ceiling in `docs/features/phases/phase-8-dashboard.md`. - [ ] Capture Lighthouse / WPT metrics before and after, attach to the PR description. - [ ] Update `docs/features/phases/phase-8-dashboard.md` with the new lazy-load contract and the trust assumption (Mermaid only loads on Graph-tab activation). ## Acceptance - Cold dashboard load with no Graph-tab interaction transfers under 200 KB and does not request `mermaid.min.js`. - First Graph-tab click loads Mermaid once; subsequent clicks reuse the cached engine without a network round-trip. - `Content-Encoding: br` (or `gzip` fallback) is present on all asset responses when the request advertises support; uncompressed bytes still served when the client opts out. - Asset SHA256 / byte-size pin tests in `crates/cognix-server/src/transport/ui/assets.rs` continue to pass against the raw bytes (compression is transport-layer, not content-layer). - New `test_dashboard_total_wire_payload_budget` test fails if the brotli'd total grows past the documented ceiling. - All existing PR #73 tests in `transport::ui` and `tests/integration/ui_graph_routes.rs` pass without modification. - No new `cargo deny` / `cargo audit` findings introduced by the compression middleware. ## References - PR: #73 — `feat: add dashboard thought graph viewer` - Code review finding: CRITICAL — eager 3.3 MB Mermaid load on every page view - Code review finding: CRITICAL — no asset compression middleware - Project rule: `.claude/rules/coding-style.md` — file/function size budgets (informs the wire-payload budget pattern)
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jesse/cognix#74
No description provided.