Every claim cites 02-PERF-BASELINE.json (archi-dev-box, real hardware).
13/15 surfaces measured cleanly; Mesh and Chat recorded as unmeasured with
reasons, never as already-fast.
Key findings:
- 4 main tabs (Home, Apps, Cloud, Fleet) measure already fast — left alone
per D-02, KeepAlive wrapping only, no data-cache conversion.
- Marketplace is the worst main tab (2033ms revisit) and the tracer pick
for 02-02, matching the user's own complaint ("often app store").
- No surface shows a serial-RPC-waterfall signature (maxConcurrentRpc is
at or near each surface's total call count everywhere multiple calls
were observed) — corrects RESEARCH.md's one "confirmed" waterfall
target, ContainerAppDetails.vue, which this session's grep confirms is
fully unreachable dead code (no importer, no route entry). D-13's
parallelization pattern has no live target in this phase's D-09 set.
- Flagged a measurement confound: 3 rows (Marketplace, MarketplaceApp-
Details, OpenWrtGateway) reach their target via a navSteps chain that
transits another main tab first, so some of their captured RPC calls are
that intermediate tab's own onMounted burst, not the destination's.
Classified conservatively (remount storm, not uncached fetch) with the
confound spelled out for 02-02/02-03 to re-verify before assuming.
- Wallet-send's revisit (2607ms) is consistently slower than its first
visit (735ms) across all 3 runs despite zero RPC either time — flagged
as an anomaly for 02-03 to profile directly, not hidden.
This is the last commit in this plan — the gate 02-02 through 02-08 depend
on before any neode-ui/src change lands (git diff --name-only HEAD~2..HEAD
-- neode-ui/src is empty across all three of this plan's commits).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15 KiB
Phase 02 UI Performance — Findings (D-10)
Status: Gate doc — committed before any neode-ui/src change lands in this phase (D-10).
Every claim below cites a field in 02-PERF-BASELINE.json; nothing here is inferred from
code reading alone.
Method
Target: archi-dev-box (the on-device verification target named by D-11), resolved to
http://archi-dev-box from the executing workstation. This is a real Archipelago node, not
the local mock backend or the :8100 dev preview — 02-PERF-BASELINE.json's run header
records baseUrl: "http://archi-dev-box" and commit: "a75b6709" (the harness commit the
run was taken against).
Harness: neode-ui/e2e/perf/{surfaces,measure,surface-perf.spec}.ts (built and committed
in this plan's Task 1). Run command:
cd neode-ui
ARCHY_BASE_URL=http://archi-dev-box \
ARCHY_PASSWORD=<the node's real UI password, provided out-of-band — never committed> \
ARCHY_PERF_OUT=../.planning/phases/02-ui-performance/02-PERF-BASELINE.json \
npx playwright test e2e/perf/surface-perf.spec.ts --project=chromium --reporter=line
Sample count: 3 runs per surface (runs: 3 in the artifact header), each run
measuring a fresh first-visit (navigate from dashboard Home to the surface) and a revisit
(navigate to the neutral Settings tab — or close the surface's own trigger, for the
wallet-send modal — then return via the same UI-click path) in the same authenticated
session. 02-PERF-BASELINE.json's firstVisitMs/revisitMs/revisitRpcCount top-level
fields are the median across successful runs; samples holds every individual run
(including any per-run errors) so no data is discarded.
Field meanings:
firstVisitMs/revisitMs— wall-clock ms from the click that starts navigation to the surface'scontentSelectorbecoming visible.revisitRpcCount— number of POSTs to/rpc/v1observed during the revisit window (method name + timing only, never bodies — see the harness's threat mitigation).maxConcurrentRpc— peak number of RPC calls in flight simultaneously during the revisit, derived by sweep-line over each call's[start, start+duration]interval. A waterfall showsmaxConcurrentRpc: 1with 2+ calls; a parallel fan-out showsmaxConcurrentRpcclose to the total call count.remounted— read back from a dataset-stamp on the view's root element after the away/back round-trip.truemeans the component instance was destroyed and recreated (expected everywhere right now —grep -r "KeepAlive" neode-ui/srcreturns 0 hits, confirmed again this session).
All 15 SURFACES rows were run against archi-dev-box. 13 of 15 produced clean
first-visit/revisit/RPC/remount numbers. 2 are recorded as unmeasured with the specific
reason (never as "already fast" — see the Mesh and Chat rows below).
A methodological caveat that materially affects three rows' RPC evidence: several
navSteps chains reach their target surface by first clicking through another main tab
(Marketplace and its own AppDetails go through Home's "Browse Store" link; OpenWrtGateway
goes through Server's "OpenWrt Gateway" link). Home.vue and Server.vue both fire a burst of
RPCs from their own onMounted the instant they're briefly mounted mid-chain
(system.stats, fips.status, vpn.status/vpn.list-peers, wallet.*,
network.list-interfaces, tor.list-services, etc.). Those in-flight requests are still
resolving when the chain lands on the destination surface, and the harness's RPC tracker —
which starts recording at the top of the revisit click-chain, not at the destination
surface's own mount — captures them as if they belonged to the destination. This was
confirmed by comparing captured method names directly: OpenWrtGateway's captured set
(vpn.list-peers, network.list-interfaces, tor.list-services, fips.status) is an
exact subset of Server.vue's own 8-call set measured cleanly on the server row (whose
navSteps do not transit another tab). Where this confound applies, the affected row's
Primary Cause is classified conservatively (remount storm, the reading the elevated
revisitMs alone supports) rather than uncached fetch, and the confound is called out
explicitly in that row's Intended Fix column so 02-02/02-03 re-verify with DevTools before
assuming the RPC count is real. Rows whose navSteps reach their target directly (Apps,
Cloud, Mesh, Server, Web5, Chat, AppDetails, CloudFolder) have no such confound — their RPC
counts are trustworthy as measured.
Per-Surface Findings
| Surface | First visit (ms, median) | Revisit (ms, median) | Revisit RPC count | Max concurrent RPC | Remounted | Primary cause | Intended fix | Owning plan |
|---|---|---|---|---|---|---|---|---|
| Home (wallet figures) | 354 | 390 | 1 | 1 | true | already fast | Left alone (D-02) — still gets wrapped in <KeepAlive> per D-01 for scroll/animation-state persistence, no data-cache conversion needed |
02-04 (KeepAlive wrapping only) |
| Apps (My Apps) | 359 | 267 | 1 | 1 | true | already fast | Left alone (D-02) — KeepAlive wrapping only | 02-04 (KeepAlive wrapping only) |
| Marketplace (App Store) | 1228 | 2033 | 13 | 12 | true | remount storm | KeepAlive + convert to useCachedResource; RPC count is confounded by the Home-transit artifact above — do not treat 13 calls as Marketplace's own fetch pattern without re-verifying via DevTools |
02-02 (tracer/app store) |
| Discover (App Store tab) | 1117 | 1083 | 1 | 1 | true | remount storm | Convert to useCachedResource alongside Marketplace (same app-store surface family, D-09) |
02-02 (tracer/app store) |
| Cloud / Files | 463 | 247 | 0 | 0 | true | already fast | Left alone (D-02) — already on useCachedResource per code read; still gets KeepAlive wrapping |
02-04 (KeepAlive wrapping only) |
| Mesh | — | — | — | — | — | unmeasured | .mesh-status-grid never left the hidden/loading branch within the 20s wait window across all 3 runs — the device did not report connected on this node in the window measured. Re-run 02-05's own on-node pass once implementing; do not assume already-fast |
02-05 (Mesh) |
| Server (Network) | 969 | 738 | 8 | 7 | true | uncached fetch | Convert Server's 7–8 independent fire-and-forget onMounted calls to useCachedResource (clean measurement — navSteps reach Server directly, no transit confound) |
02-06 (Server and Home) |
| Web5 | 307 | 566 | 0 | 0 | true | remount storm | KeepAlive wrapping — 0 RPC on revisit confirms the sub-cards' existing useCachedResource usage already prevents refetch; remaining cost is render/mount of the multi-card tree |
02-04 (KeepAlive wrapping only) |
| Fleet | 376 | 330 | 0 | 0 | true | already fast | Left alone (D-02) — still gets KeepAlive wrapping | 02-04 (KeepAlive wrapping only) |
| Chat (AIUI) | — | — | — | — | — | unmeasured | AIUI's own "connecting" overlay stayed up long enough on this node that the harness's close-button click never resolved within its retry budget (.chat-close-btn timeout, 20s) in run 0; runs 1–2 timed out even earlier at 6.6s once the harness's stuck-state recovery kicked in. This is itself a data point worth carrying into 02-07: AIUI's connection/handshake latency on real hardware is materially longer than on the dev workstation |
02-07 (Chat/AIUI) |
| AppDetails (secondary) | 1040 | 1204 | 2 | 1 | true | uncached fetch | Convert loadBitcoinSync()/loadCredentials() to keyed useCachedResource (clean measurement — navSteps transit Apps, not Home, no confound) |
02-03 (secondary screens) |
| MarketplaceAppDetails (secondary) | 1978 | 1793 | 14 | 12 | true | remount storm | Convert to keyed useCachedResource; 13 of the 14 captured calls match the same Home-transit confound as Marketplace — only package.versions (the 14th call, firing later at +1459ms, well after the Home-mount burst settles) is trustworthy as this view's own fetch |
02-03 (secondary screens) |
| CloudFolder (secondary) | 5116 | 1212 | 0 | 0 | true | remount storm | First visit is consistently ~5s (lazy route-chunk cold load, confirmed across runs); revisit drops to ~1.2s once the chunk is warm client-side but stays remount-bound (0 RPC either way) — convert to keyed useCachedResource for the folder listing regardless, per D-04's secondary-screen pattern |
02-03 (secondary screens) |
| OpenWrtGateway (secondary) | 3213 | 663.5 | 4 | 4 | true | remount storm | RPC count is confounded by the Server-transit artifact above — the captured methods are an exact subset of Server.vue's own call set, not evidence of OpenWrtGateway's own fetch pattern. Re-verify with DevTools before deciding whether this page needs its own useCachedResource conversion |
02-03 (secondary screens) |
| Wallet / send flow (Home wallet card → SendBitcoinModal) | 735 | 2607 | 0 | 0 | true | remount storm | Anomaly worth flagging, not hiding: revisit (2607ms median) is consistently slower than first visit (735ms median) across all 3 runs (2562/2619/2607 vs 857/735/607) — the modal always fully remounts (BaseModal's v-if) with zero RPC either time, so the extra ~1.9s on reopen is pure client-side cost (fee/balance recompute, store re-subscription, or similar) that should be profiled directly with DevTools during 02-03, not assumed away |
02-03 (secondary screens) |
Ranked Fix Order
Worst-revisit-first, main tabs and secondary screens together (medians from the table
above; — rows excluded, listed separately below):
- Wallet-send — 2607ms (secondary/modal; anomalous revisit > first-visit, see note above)
- Marketplace — 2033ms (main tab; RPC evidence confounded, see caveat)
- MarketplaceAppDetails — 1793ms (secondary; RPC evidence confounded, see caveat)
- CloudFolder — 1212ms (secondary; first-visit-dominant, ~5s cold chunk load)
- AppDetails — 1204ms (secondary; clean uncached-fetch evidence)
- Discover — 1083ms (secondary; remount storm)
- Server — 738ms (main tab; clean uncached-fetch evidence)
- OpenWrtGateway — 663.5ms (secondary; RPC evidence confounded)
- Web5 — 566ms (main tab; remount storm, 0 RPC)
- Home — 390ms (main tab; already fast)
- Fleet — 330ms (main tab; already fast)
- Apps — 267ms (main tab; already fast)
- Cloud — 247ms (main tab; already fast)
Unmeasured: Mesh, Chat — see their rows above for why, and do not slot them into this ranking as if they were fast.
Tracer tab for plan 02-02: among main tabs specifically (02-02's actual scope — wallet-send and the secondary screens are not tabs), Marketplace is the worst-performing surface (2033ms revisit) and is also the surface the user named first ("often app store" — D-09). It is the tracer pick for 02-02, consistent with the plan set's existing "02-02 tracer/app store" naming.
Surfaces Left Alone (D-02)
These measured already fast on real hardware — no useCachedResource conversion is
planned for their data layer. Per D-01 they still get wrapped in <KeepAlive> as part of
02-04's shared Dashboard.vue restructure (scroll/animation-state persistence is free and
harmless even for an already-fast view), but that is template plumbing, not a per-view
data-caching change, so they are listed here rather than under any per-surface fix plan.
| Surface | First visit (ms) | Revisit (ms) | Revisit RPC | Why left alone |
|---|---|---|---|---|
| Home | 354 | 390 | 1 | Sub-400ms revisit with a single trivial system.get-hostname ping (not view-specific wallet data); wallet figures render from already-reactive store state |
| Apps | 359 | 267 | 1 | Sub-300ms revisit, single trivial ping; confirms RESEARCH.md's prediction that Apps.vue's data is WebSocket-pushed, not per-mount-fetched |
| Cloud | 463 | 247 | 0 | Sub-250ms revisit, zero RPC — confirms Cloud.vue's existing useCachedResource usage (peersResource/countsResource) is already preventing refetch |
| Fleet | 376 | 330 | 0 | Sub-350ms revisit, zero RPC |
Corrections to Prior Research
ContainerAppDetails.vue dead-code claim — CONFIRMED, stronger than RESEARCH.md stated.
RESEARCH.md's Assumptions & Flagged Items (FA-B) reported the planner's grep found "only a
self-referential comment inside the file itself." Re-running the identical grep this
session found zero matches at all, including no self-reference:
$ grep -rn "ContainerAppDetails" neode-ui/src
(no output)
The file exists at neode-ui/src/views/ContainerAppDetails.vue (351 lines) but has no
importer anywhere in neode-ui/src and no route entry in neode-ui/src/router/index.ts
(verified directly against the full route table read for this plan's Task 1). It is
unreachable dead code.
Impact: RESEARCH.md named ContainerAppDetails.vue:169-172's
await loadContainer(); await loadLogs(); await loadHealthStatus() as the phase's one
confirmed serial-RPC-waterfall fix target (Pattern 3). Since the file is unreachable, no
plan in this phase should spend effort parallelizing it — doing so would touch dead code
for zero user-visible benefit. More importantly: no other surface in the measured D-09 set
shows a serial-waterfall signature. Every surface in this findings doc with 2+ RPC calls on
revisit (Marketplace, Server, AppDetails, MarketplaceAppDetails, OpenWrtGateway) shows
maxConcurrentRpc at or near its total call count (7 of 8, 12 of 13, 12 of 14, 1 of 1-2, 4 of
4) — i.e. already parallel, not serial. serial RPC waterfall is not used as a Primary
Cause anywhere in the Per-Surface Findings table above because the measured evidence does
not support it for any reachable surface. This is the single most consequential correction
this profiling pass makes to RESEARCH.md: D-13's Promise.all/Promise.allSettled
parallelization pattern has no live target to apply to in this phase's D-09 surface set.
Owning Plans
| Surface | Owning plan |
|---|---|
| Home | 02-04 (KeepAlive wrapping only — see Surfaces Left Alone) / 02-06 names Home in its title but the profiling shows no data-layer work is needed there |
| Apps | 02-04 (KeepAlive wrapping only) |
| Marketplace | 02-02 (tracer/app store) |
| Discover | 02-02 (tracer/app store) |
| Cloud | 02-04 (KeepAlive wrapping only) |
| Mesh | 02-05 (Mesh) |
| Server | 02-06 (Server and Home) |
| Web5 | 02-04 (KeepAlive wrapping only) |
| Fleet | 02-04 (KeepAlive wrapping only) |
| Chat | 02-07 (Chat/AIUI) |
| AppDetails | 02-03 (secondary screens) |
| MarketplaceAppDetails | 02-03 (secondary screens) |
| CloudFolder | 02-03 (secondary screens) |
| OpenWrtGateway | 02-03 (secondary screens) |
| Wallet / send flow | 02-03 (secondary screens) |
The shared foundation both 02-02 and every other per-surface plan depend on — the
Dashboard.vue nested-<router-view> KeepAlive restructure and the useCachedResource
onActivated extension — is owned by 02-04, per RESEARCH.md's own recommendation to
land that shared work before converting individual views.