docs(02-11): plan gap closure for six confirmed timing regressions

Fleet/AppDetails/Web5/OpenWrtGateway/Server/Discover all measured
slower on revisit than the pre-phase-2 baseline (02-10's three-way
dispersion analysis). This plan profiles the real cause per surface
before touching source (D-10), fixes what's fixable without
reverting any T-02-01 persist:false decision or touching the
visual/animation contract, and re-measures with the frozen harness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-31 08:11:44 -04:00
co-authored by Claude Opus 5
parent 516c3bfa07
commit ebd0afa68b
@@ -0,0 +1,214 @@
---
phase: 02-ui-performance
plan: 11
type: execute
wave: 8
depends_on: ["02-10"]
files_modified:
- .planning/phases/02-ui-performance/02-FINDINGS.md
- .planning/phases/02-ui-performance/02-PERF-FINAL.json
- neode-ui/e2e/perf/profile-revisit.spec.ts
- neode-ui/src/views/web5/Web5.vue
- neode-ui/src/views/Server.vue
- neode-ui/src/views/Discover.vue
- neode-ui/src/views/AppDetails.vue
- neode-ui/src/views/server/OpenWrtGateway.vue
- neode-ui/src/views/Fleet.vue
- neode-ui/src/composables/useCachedResource.ts
- neode-ui/src/views/dashboard/dashboardViewWrappers.ts
- neode-ui/src/views/dashboard/DashboardRouterView.vue
- neode-ui/src/composables/useControllerNav.ts
autonomous: true
gap_closure: true
requirements: [PERF-02, PERF-03]
user_setup:
- service: archi-dev-box (the node's own UI login)
why: "Both the diagnostic profiling pass and the final re-measure drive a real authenticated browser session against the deployed build on archi-dev-box — the same D-11 on-device target every prior perf plan in this phase used."
env_vars:
- name: ARCHY_PASSWORD
source: "Supplied out-of-band by the coordinator at runtime. Passed only as an environment variable on the harness/profiling command line — never written to a file, commit, or planning artifact. Redact as [supplied] in all committed prose."
must_haves:
truths:
- "Before any source file changes, a real CPU/rendering profile (CDP Profiler + Tracing domains — the same underlying data DevTools' Performance panel visualizes) has been captured for each of the six surfaces' revisit window, and 02-FINDINGS.md names, per surface, WHERE the milliseconds actually go, cited to specific profile fields — not inferred from source reading alone"
- "Fleet ends with an explicit verdict and a re-measured number, not just a root-cause guess — it is the worst specimen (330ms baseline -> 2631ms) and this plan's clearest test of whether the named cause and the fix are correct"
- "AppDetails ends with an explicit verdict and a re-measured number"
- "Web5 ends with an explicit verdict and a re-measured number"
- "OpenWrtGateway ends with an explicit verdict and a re-measured number"
- "Server ends with an explicit verdict and a re-measured number"
- "Discover ends with an explicit verdict and a re-measured number"
- "Every fix applied preserves the security decisions this phase already made (T-02-01 persist:false on wallet/identity keys stays false; no re-enabling sessionStorage persistence to shave milliseconds) and the visual/animation contract keepAliveTabs.test.ts pins (structural DOM-shape assertions stay green, byte-for-byte unmodified test file)"
- "The re-measure uses the frozen 02-01 harness unmodified (git diff --stat against the harness's three core files is empty) so 02-PERF-FINAL.json stays directly comparable to BASELINE/AFTER/REMEASURE"
- "First-paint (when a human would perceive content) is reported separately from contentSelector-visible (the harness's stricter measured field) for every surface where they diverge meaningfully, so the report doesn't overstate how bad a fixed/irreducible cost feels in practice"
artifacts:
- ".planning/phases/02-ui-performance/02-FINDINGS.md — new `## Client-Side Render Cost Root Cause (gap closure)` section: profiling method, per-surface evidence, named cause, fix (or irreducible-cost justification), before/after numbers"
- ".planning/phases/02-ui-performance/02-PERF-FINAL.json — fourth on-device measurement, 5 runs/surface, against the fixed build"
- "neode-ui/e2e/perf/profile-revisit.spec.ts — the additive CDP-profiling diagnostic script (does not touch the frozen harness files)"
key_links:
- "02-PERF-FINAL.json -> 02-PERF-BASELINE.json / 02-PERF-AFTER.json / 02-PERF-REMEASURE.json — joined by surface `id`, four-way comparison on `revisitMs`"
- "Each named cause in 02-FINDINGS.md -> the specific CPU-profile/trace-event/animation-log field that supports it"
---
<objective>
Phase 02's last open item: six surfaces are measurably SLOWER on revisit than the pre-phase-2 baseline, confirmed real (not noise) by 02-10's three-way dispersion analysis — Fleet (330ms -> 2631ms, worst, ~8x), AppDetails (1204ms -> 2668ms), Web5 (566ms -> 1329ms), OpenWrtGateway (664ms -> 1460ms), Server (738ms -> 1239ms), Discover (1083ms -> 1453ms). RPC counts are flat-or-improved on all six (confirmed in 02-FINDINGS.md's three-way table) and, for Server/Web5, the component instance is independently proven (02-09) to genuinely survive the round-trip — so this is NOT network and NOT a remount. It is client-side render/reactivity cost that KeepAlive caching made visible by removing the network wait that used to hide it.
D-10 discipline applies to this plan as it did to every plan before it: name the measured cause — with real profiling evidence, not a plausible-sounding hypothesis — before touching any source file. 02-10 named a class ("split-signal render/reactivation cost") but did not pinpoint a mechanism per surface; that is this plan's Task 1.
The target is NOT "restore the old baseline" — those baselines were already poor. A revisit to a KeepAlive-cached surface shows an existing component instance with data already in memory: no mount, no fetch, no layout from scratch. Pass bar: revisit ms under 300ms. Target: under 150ms. Where a surface genuinely cannot reach that, the plan must say so with profiling evidence naming the irreducible cost, not quietly settle at baseline and call it fixed.
Hard constraints carried into every task below: security decisions (T-02-01 `persist:false` on wallet/identity cache keys) are non-negotiable and stay exactly as they are regardless of any timing cost they impose. No visual or animation change — `keepAliveTabs.test.ts` structurally pins the dashboard DOM shape and must stay green and byte-for-byte unmodified. Money surfaces never present cached data as live (D-05/T-02-01, unaffected by this plan's scope). Frontend-only deploy to archi-dev-box, never the fleet/OTA path.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@CLAUDE.md
@.planning/phases/02-ui-performance/02-VERIFICATION.md
Read only these parts of the large phase artifacts:
- `.planning/phases/02-ui-performance/02-FINDINGS.md``## Method`, the `## Re-measurement (gap closure)` section (three-way table, per-surface verdicts, Accepted Deviations table) — this is the exact gap this plan closes.
- `.planning/phases/02-ui-performance/02-REVIEW.md` — the CR-01 follow-up (persist-schema-migration mechanism) and the "persist is now required, not defaulted" call-site table — every fix in this plan must preserve every `persist:false` decision recorded there.
- `.planning/phases/02-ui-performance/02-02-SUMMARY.md` and `02-04-SUMMARY.md` — the KeepAlive wrapper architecture (`DashboardRouterView.vue`/`dashboardViewWrappers.ts`) and the lifecycle-audit bucket convention (once-per-session / every-entry / only-while-visible), so any fix follows the established pattern rather than inventing a new one.
- `neode-ui/src/composables/useCachedResource.ts`, `neode-ui/src/stores/resources.ts` — the shared caching layer every affected surface uses.
Do not re-read `02-PERF-BASELINE.json` / `02-PERF-AFTER.json` / `02-PERF-REMEASURE.json` in full — extract fields with `node -e`/`jq` as needed.
</context>
<tasks>
<task type="auto">
<name>Task 1: Profile each surface's revisit window and name the real cause per surface</name>
<files>neode-ui/e2e/perf/profile-revisit.spec.ts, .planning/phases/02-ui-performance/02-FINDINGS.md</files>
<precondition>`ARCHY_PASSWORD` is exported in the environment and `curl -sS -o /dev/null -w '%{http_code}' http://archi-dev-box/` returns 2xx/3xx. If a `cargo build`/`rustc` process is active on this shared box (`pgrep -af 'cargo|rustc'`), wait for it to finish before taking ANY timed measurement — a concurrent full workspace rebuild saturates this machine's CPU and would corrupt the numbers exactly the way 02-10's three-way analysis worked to rule out.</precondition>
<read_first>
`neode-ui/e2e/perf/surfaces.ts` (read-only, for `navSteps`/`contentSelector`/`rootSelector` — do not modify) and `neode-ui/e2e/perf/keepalive-remount-probe.spec.ts` (the 02-09 precedent for an additive, frozen-harness-respecting diagnostic spec: login helper, click-with-guard retry pattern).
</read_first>
<action>
Build (or extend, if already present from an earlier session in this plan) a standalone, additive Playwright spec — `neode-ui/e2e/perf/profile-revisit.spec.ts` — that does NOT edit `surfaces.ts`/`measure.ts`/`surface-perf.spec.ts` (confirm via `git diff --stat` against those three files before and after this task).
For each of the six named surfaces (Web5, Server, Discover, AppDetails, OpenWrtGateway, Fleet), reproduce the harness's own first-visit -> away -> revisit structure, then during the revisit window ONLY, capture:
1. A CDP `Profiler` (CPU) sample profile (`Profiler.start`/`Profiler.stop`), aggregated by self-time per function AND per deployed chunk (vendor/index/per-route chunk — production has no sourcemaps, so bucket by chunk name, not node_modules path).
2. A CDP `Tracing` capture (categories including `disabled-by-default-devtools.timeline`) — the same data DevTools' Performance panel renders — aggregated by event name (Layout, RecalculateStyles, Paint, CompositeLayers, RunTask, TimerFire, ...) on the renderer main thread, so rendering-pipeline cost (which a bare JS CPU profile can only see as generic "(program)"/"(idle)") is visible by name.
3. Every `setTimeout`/`requestAnimationFrame` call scheduled during the window (delay value, count) via a runtime monkey-patch, restored after read-back.
4. Every RPC call's wall-clock start/duration during the window (method name + timing only, no bodies — T-02-06 convention).
5. Every CSS `transitionrun`/`transitionend`/`animationstart`/`animationend` event on `document` during the window (type, property/animation name, target tag+class, elapsed time) — decisive evidence for whether a CSS transition duration (which does NOT block Playwright's `visible` check — only non-empty bounding box + not `visibility:hidden` — but DOES affect what a human perceives) is what dominates the wall clock.
6. A first-paint timestamp independent of Playwright's own `waitFor({state:'visible'})`: a `requestAnimationFrame`-driven poll recording the instant `contentSelector` first gets a non-empty bounding box, so first-paint and content-visible can be reported as two distinct numbers.
Run it against archi-dev-box and capture the full output. For each surface, read the category/function/trace breakdown and name the DOMINANT real cost — do not guess from source reading alone; if the profile is ambiguous, add more instrumentation (e.g. per-component `console.time`/`performance.mark` if trace categories aren't decisive) rather than asserting a hypothesis the data doesn't support.
Write a new `## Client-Side Render Cost Root Cause (gap closure)` section into `02-FINDINGS.md`: one subsection per surface, citing the specific profile/trace/timer/animation-log evidence, naming the mechanism (e.g. "N ms in Layout/RecalculateStyles scaling with subtree size," "N chained rAF calls before contentSelector's bounding box is non-empty," "a debounced/every-entry `useCachedResource` `onActivated` cascade re-triggering M resources," "the navSteps chain transits another KeepAlive'd main tab whose own reactivation cost bleeds into this surface's measured window"). Commit this findings section BEFORE starting Task 2's source edits (D-10 gate — verified by commit-order, same discipline 02-01/02-09 used).
</action>
<verify>
<automated>git diff --stat 3ee20430 -- neode-ui/e2e/perf/surfaces.ts neode-ui/e2e/perf/measure.ts neode-ui/e2e/perf/surface-perf.spec.ts (must print nothing — the instrument stays frozen)</automated>
<automated>grep -q "## Client-Side Render Cost Root Cause (gap closure)" .planning/phases/02-ui-performance/02-FINDINGS.md && for s in Web5 Server Discover AppDetails OpenWrtGateway Fleet; do grep -q "$s" .planning/phases/02-ui-performance/02-FINDINGS.md && echo "present: $s"; done</automated>
</verify>
<done>02-FINDINGS.md names, with cited profiling/trace evidence, the real dominant cost for each of the six surfaces' revisit-ms cost — committed before any neode-ui/src file changes in this plan.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Fix each surface's named cause where fixable; record irreducible-cost justification where not</name>
<files>neode-ui/src/views/web5/Web5.vue, neode-ui/src/views/Server.vue, neode-ui/src/views/Discover.vue, neode-ui/src/views/AppDetails.vue, neode-ui/src/views/server/OpenWrtGateway.vue, neode-ui/src/views/Fleet.vue, neode-ui/src/composables/useCachedResource.ts, neode-ui/src/views/dashboard/dashboardViewWrappers.ts, neode-ui/src/views/dashboard/DashboardRouterView.vue, neode-ui/src/composables/useControllerNav.ts, .planning/phases/02-ui-performance/02-FINDINGS.md</name>
<behavior>
For each surface whose Task 1 evidence names a fixable client-side cost (expensive computed re-evaluation, watcher cascade, whole-subtree re-render instead of re-attach, per-row recompute, unbounded rAF/timer chain, a navSteps transit-confound that is itself measuring another tab's real cost) apply the smallest change that removes the named cost, guarded by a test that fails before the fix and passes after. Where the named cost is genuinely irreducible (e.g., the security-mandated absence of sessionStorage persistence for wallet/identity keys forcing a real re-fetch every session-boot; or a navSteps chain that legitimately must transit another main tab because Fleet has no direct sidebar entry) do not force a fix — document the irreducible cost with its profiling evidence in 02-FINDINGS.md instead, and say so plainly rather than leaving the surface unaddressed.
</behavior>
<action>
Work surface by surface, smallest diff first. Do not touch a file this task doesn't need to for a given surface's fix.
Guardrails that apply to every fix in this task, no exceptions:
- Never re-enable `persist: true`/sessionStorage writes on any key T-02-01/CR-01 decided must be `persist: false` (`web5.lnd-info`, `web5.networking-profits`, `server.fips-summary`, and every other key in 02-REVIEW.md's persist-decision table) — if a fix's instinct is "cache this in sessionStorage to skip the re-fetch," that instinct is wrong for these keys; find a different mechanism (e.g. in-memory-only staleness widening, avoiding redundant re-render, not adding storage).
- Never change `keepAliveTabs.test.ts` and never change any visible layout, spacing, animation timing/easing, or transition name — run this file's existing assertions after every change in this task and treat any diff in its output as a hard stop, not a thing to update.
- If a fix requires touching `useCachedResource.ts`, `dashboardViewWrappers.ts`, or `DashboardRouterView.vue` (shared by every KeepAlive'd surface, not just the six named here), verify the change against the FULL existing `keepAliveLifecycle.test.ts` + `keepAliveTabs.test.ts` suite, not just the affected surface's own tests — a shared-composable/host change with an unseen side effect on an unrelated tab is exactly the class of regression 02-04's own bare-mount bug demonstrated this phase.
- If a navSteps transit-confound is Task 1's named cause for a surface (e.g. Fleet transiting Web5, OpenWrtGateway transiting Server, Discover/AppDetails transiting Apps), the fix target is the TRANSITED tab's own reactivation cost, not the destination surface — fixing Web5's reactivation cost should show up as an improvement in Fleet's measured number too; say so explicitly if that's what happens, rather than fixing the same mechanism twice under two different surface names.
- Every fix gets a regression test in the relevant `__tests__` file (or a new one, following the `CloudPeersRefresh.test.ts`/`MarketplaceRefresh.test.ts` precedent when an existing test file's `vi.mock('vue-router')` would conflict) that fails on the pre-fix code and passes after.
After each surface's fix (or irreducible-cost decision) lands, append its outcome to the `## Client-Side Render Cost Root Cause (gap closure)` section in `02-FINDINGS.md`: what changed, why, the regression test that pins it, and — once Task 3's re-measure exists — the before/after number.
Run the full suite, `npm run type-check`, and `npm run build` (grep the built bundle for a new string introduced by at least one fix, per CLAUDE.md's silent-no-op-build caution) after all six surfaces are addressed, before moving to Task 3.
</action>
<verify>
<automated>cd neode-ui && npm test 2>&1 | tail -15</automated>
<automated>cd neode-ui && npm run type-check</automated>
<automated>git diff --stat -- neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts (must print nothing — structural visual contract untouched)</automated>
</verify>
<done>Every one of the six surfaces has either a landed, test-covered fix or an explicit, evidence-backed irreducible-cost note in 02-FINDINGS.md. Full suite, type-check and build are green. keepAliveTabs.test.ts is byte-for-byte unmodified and still passing. No `persist:false` decision was reverted.</done>
</task>
<task type="auto">
<name>Task 3: Deploy, re-measure with the frozen harness, and write the four-way verdict</name>
<files>.planning/phases/02-ui-performance/02-PERF-FINAL.json, .planning/phases/02-ui-performance/02-FINDINGS.md, .planning/REQUIREMENTS.md</files>
<precondition>No `cargo`/`rustc` process is active on this shared box (`pgrep -af 'cargo|rustc'`) — re-check immediately before both the deploy and the harness run, not just once at the start of this task, since a build can start mid-task on a shared machine.</precondition>
<action>
Confirm the working tree's `neode-ui/src` changes from Task 2 are the ONLY uncommitted/unpushed changes about to ship — `git status --short -- neode-ui/src` should show only this plan's own files. If another session's uncommitted work is present in `neode-ui/src` at deploy time (the exact hazard 02-10 hit), stop and report it rather than deploying it alongside this plan's fix, per the deploy-safety rule carried in this plan's context.
Deploy frontend-only from the MAIN checkout (never a worktree — this box IS archi-dev-box over loopback, so a worktree-sourced deploy is the exact hazard that previously destroyed ~1810 tracked files): `ARCHIPELAGO_TARGET=archipelago@archi-dev-box scripts/deploy-to-target.sh --frontend-only`. Confirm the post-deploy health check and `scripts/deploy-history.log`'s new entry (`dirty=false`, the commit hash matching Task 2's last commit).
Re-run the frozen harness exactly as 02-10 did: `cd neode-ui && ARCHY_BASE_URL=http://archi-dev-box ARCHY_PERF_RUNS=5 ARCHY_PERF_OUT=../.planning/phases/02-ui-performance/02-PERF-FINAL.json npx playwright test e2e/perf/surface-perf.spec.ts --project=chromium --reporter=line`, with `ARCHY_PASSWORD` exported. Confirm `git diff --stat` against the three frozen harness files is still empty.
Build a four-way comparison (BASELINE -> AFTER -> REMEASURE -> FINAL) for all six named surfaces, dispersion (min/median/max) not bare medians, the same discipline 02-10 established. For each surface, write an explicit verdict into `02-FINDINGS.md`:
- **Fixed**: revisit ms at or below the pass bar (300ms) or the stretch target (150ms), with the number and the mechanism that closed the gap.
- **Improved but not yet under the pass bar**: report the number honestly, explain the remaining gap against the profiling evidence (do not round up to "fixed").
- **Irreducible/justified**: the profiling evidence from Task 1/2 naming exactly why (e.g. a security-mandated re-fetch cost with no persistence to shortcut it), with the number recorded as the accepted floor.
Report first-paint (from Task 1's independent probe, re-run once against the final build if useful for the write-up) separately from contentSelector-visible for any surface where they diverge meaningfully, so the report states plainly whether the fix also improved the human-perceived experience, not just the harness's stricter metric.
Update `.planning/REQUIREMENTS.md`'s PERF-02/PERF-03 rows to point at this final verdict. Commit and push (`git add` by path, Co-Authored-By trailer, `git push gitea-ai main`).
</action>
<verify>
<automated>test -f .planning/phases/02-ui-performance/02-PERF-FINAL.json && node -e "const r=require('./.planning/phases/02-ui-performance/02-PERF-FINAL.json'); console.log('rows:', (r.surfaces||r.results||r).length||Object.keys(r).length, 'baseUrl:', r.baseUrl, 'runs:', r.runs)"</automated>
<automated>git diff --stat 3ee20430 -- neode-ui/e2e/perf/surfaces.ts neode-ui/e2e/perf/measure.ts neode-ui/e2e/perf/surface-perf.spec.ts (must print nothing)</automated>
<automated>for s in Web5 Server Discover AppDetails OpenWrtGateway Fleet; do grep -A2 "^\*\*$s" .planning/phases/02-ui-performance/02-FINDINGS.md | head -3; done</automated>
</verify>
<done>02-PERF-FINAL.json exists (5 runs/surface, archi-dev-box, frozen harness). Every one of the six named surfaces (Web5, Server, Discover, AppDetails, OpenWrtGateway, Fleet) has an explicit fixed/improved/irreducible verdict with a real number in 02-FINDINGS.md. REQUIREMENTS.md reflects the final state. Work is committed and pushed.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| operator workstation -> archi-dev-box UI login | The real node password crosses this boundary at runtime to drive both the profiling script and the final re-measure |
| profiling script -> local console output | CPU profiles / trace events / RPC method names are printed to the terminal transcript, never written to a committed artifact verbatim |
| fixed build -> `/opt/archipelago/web-ui` on a real node | Task 3's frontend-only deploy |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-02-11-01 | Information Disclosure | `ARCHY_PASSWORD` | high | mitigate | Environment variable only, never inlined into a committed command, never echoed into 02-FINDINGS.md or the SUMMARY. |
| T-02-11-02 | Information Disclosure | `profile-revisit.spec.ts` output | medium | mitigate | RPC tracking captures method name + timing only, no bodies (matches the frozen harness's own T-02-06 mitigation exactly). |
| T-02-11-03 | Tampering | any `persist:false` cache-key decision (T-02-01/CR-01) | critical | mitigate | Task 2's guardrails explicitly forbid re-enabling sessionStorage persistence on any wallet/identity key to shave milliseconds; every fix is checked against 02-REVIEW.md's persist-decision table before landing. |
| T-02-11-04 | Tampering | `keepAliveTabs.test.ts` (visual/structural contract) | high | mitigate | Task 2 explicitly gates every fix against this file's unmodified assertions passing; `git diff --stat` on this specific file is a Task 2 verify step. |
| T-02-11-05 | Tampering | frozen harness (`surfaces.ts`/`measure.ts`/`surface-perf.spec.ts`) | medium | mitigate | `git diff --stat` gate in Task 1 and Task 3, identical to every prior gap-closure plan this phase. |
| T-02-11-06 | Tampering | `scripts/deploy-to-target.sh --frontend-only` | medium | mitigate | Main-checkout only (never a worktree — this box is archi-dev-box itself), archi-dev-box only, no OTA/fleet path, post-deploy health check and deploy-history log entry confirmed. |
| T-02-11-SC | Tampering | npm/pip/cargo installs | high | accept | No new package-manager dependency is needed for a CDP-profiling script (Playwright is already a devDependency) or for any of the named fix classes. If one becomes necessary, halt and route through the Package Legitimacy Gate. |
</threat_model>
<verification>
- 02-FINDINGS.md names the real, profiled cause for each of the six surfaces before any source-file change (commit-order verified).
- Every one of the six surfaces ends with an explicit verdict: fixed (with numbers under the pass bar), improved-but-honest (with numbers and the remaining gap explained), or irreducible/justified (with the security or architectural reason named).
- `02-PERF-FINAL.json` exists against a fresh archi-dev-box deploy, 5 runs/surface, frozen harness confirmed via `git diff --stat`.
- Full vitest suite, type-check and build green; `keepAliveTabs.test.ts` byte-for-byte unmodified and passing.
- No `persist:false` (T-02-01) decision reverted.
- First-paint reported separately from contentSelector-visible wherever they diverge meaningfully.
</verification>
<success_criteria>
- The phase's last open item (six confirmed-real timing regressions, none previously fixed) is closed with real fixes where fixable and honest, evidenced irreducible-cost notes where not.
- No surface is left at "accepted deviation, no further action" without at least one real attempt at profiling + fixing, per the objective's explicit reframing that the old baseline is not the target.
- The visual/animation contract and every T-02-01 security decision this phase already made are provably unchanged.
</success_criteria>
<output>
Create `.planning/phases/02-ui-performance/02-11-SUMMARY.md` when done: per-surface before/after numbers across all four artifacts, first-paint vs content-visible where they diverge, what changed and why, what was deliberately left as an irreducible/justified cost, and confirmation the visual/security constraints held.
</output>