Impact: low (grouped) · Confidence: confirmed
Grouped low-impact PWA performance findings from the holistic review.
1. Production cold start blocks on a serialized /demo-manifest.json fetch
web/src/index.tsx:8 — await initializeRuntimeTransport() (runtimeTransport.ts:87-93, cache: "no-store") completes before Promise.all([import("./App"), …]) even starts. In a non-demo deployment this adds one guaranteed round trip (a 404) to first paint, in series with the App/Board chunk fetches. Fix: start the manifest fetch and import("./App") in parallel; install the transport before render.
2. scripts/check_bundle.py's "first screen" budget under-reports the real first screen
It reports 47.3KB JS, but index.tsx:19-27 unconditionally await import("./App") — the 145KB App chunk statically includes the eager Board, CommandPalette, Sessions, and FileTree (deliberate, per the comment at App.tsx:125-135) — so real pre-paint JS is ~200KB. The budget passes honestly today, but the dynamic-import indirection means the gate cannot see growth in the chunks that actually gate first paint. Fix: have the budget count App + its static imports as first-screen.
3. Editor captures a full-document draft on every keystroke
web/src/Editor.tsx:194-208 — onDidChangeModelContent calls editor.getValue() (O(document) string build), compares against savedContent (O(document)), and saveViewState() per keystroke. Fine for source files; janks on multi-MB files. Fix: debounce ~300ms; use model.getValueLength()/version-id as a cheap pre-filter before materializing the string.
4. All terminal panes stay mounted and attached forever on desktop
web/src/tabLifecycle.ts:8 (terminal → retention "always") + App.tsx:1809 (display: none when inactive): each hidden pane keeps a live WebSocket receiving and parsing PTY output into its xterm buffer (scrollback 5000). Deliberate and documented (Terminal.tsx:106-112); with many busy sessions the background parse + WS bandwidth adds up. If it shows in profiles: detach hidden panes after N minutes idle (close WS, keep the cache) and rely on the existing snapshot-resume path.
5. filteredSessions parses dates inside a sort comparator
web/src/History.tsx:481-490 (WIP code) — Date.parse in the comparator is O(n log n) parses; bounded by the 200/page load, so a micro-nit. Precompute the timestamp once per row when convenient.
(Verified well-done, for contrast: xterm.js with capped scrollback and binary WS frames; one shared SSE stream with backoff; Fenwick-tree MeasuredWindow virtualization on Board/Backlog; process-wide taxonomy cache; route-lazy heavy surfaces with a budget-gated 47KB first screen; no-cache service worker; historyReplay.ts linear and bounded.)
From a holistic security & performance review, 2026-09-02, branch feat/product-version @ 3933a09; line numbers refer to that tree.
Impact: low (grouped) · Confidence: confirmed
Grouped low-impact PWA performance findings from the holistic review.
1. Production cold start blocks on a serialized
/demo-manifest.jsonfetchweb/src/index.tsx:8—await initializeRuntimeTransport()(runtimeTransport.ts:87-93,cache: "no-store") completes beforePromise.all([import("./App"), …])even starts. In a non-demo deployment this adds one guaranteed round trip (a 404) to first paint, in series with the App/Board chunk fetches. Fix: start the manifest fetch andimport("./App")in parallel; install the transport beforerender.2.
scripts/check_bundle.py's "first screen" budget under-reports the real first screenIt reports 47.3KB JS, but
index.tsx:19-27unconditionallyawait import("./App")— the 145KB App chunk statically includes the eager Board, CommandPalette, Sessions, and FileTree (deliberate, per the comment atApp.tsx:125-135) — so real pre-paint JS is ~200KB. The budget passes honestly today, but the dynamic-import indirection means the gate cannot see growth in the chunks that actually gate first paint. Fix: have the budget countApp+ its static imports as first-screen.3. Editor captures a full-document draft on every keystroke
web/src/Editor.tsx:194-208—onDidChangeModelContentcallseditor.getValue()(O(document) string build), compares againstsavedContent(O(document)), andsaveViewState()per keystroke. Fine for source files; janks on multi-MB files. Fix: debounce ~300ms; usemodel.getValueLength()/version-id as a cheap pre-filter before materializing the string.4. All terminal panes stay mounted and attached forever on desktop
web/src/tabLifecycle.ts:8(terminal → retention "always") +App.tsx:1809(display: nonewhen inactive): each hidden pane keeps a live WebSocket receiving and parsing PTY output into its xterm buffer (scrollback 5000). Deliberate and documented (Terminal.tsx:106-112); with many busy sessions the background parse + WS bandwidth adds up. If it shows in profiles: detach hidden panes after N minutes idle (close WS, keep the cache) and rely on the existing snapshot-resume path.5.
filteredSessionsparses dates inside a sort comparatorweb/src/History.tsx:481-490(WIP code) —Date.parsein the comparator is O(n log n) parses; bounded by the 200/page load, so a micro-nit. Precompute the timestamp once per row when convenient.(Verified well-done, for contrast: xterm.js with capped scrollback and binary WS frames; one shared SSE stream with backoff; Fenwick-tree
MeasuredWindowvirtualization on Board/Backlog; process-wide taxonomy cache; route-lazy heavy surfaces with a budget-gated 47KB first screen; no-cache service worker;historyReplay.tslinear and bounded.)From a holistic security & performance review, 2026-09-02, branch
feat/product-version@ 3933a09; line numbers refer to that tree.