perf(viewer): lite snapshot + on-demand content + instant last-visit restore (10.2s → 1.6s cold @10Mbps)#254
Merged
Merged
Conversation
…visit restore
The load path shipped the ENTIRE workspace on every page load: the SSE snapshot
serialized all boards' full content into one uncompressed event — measured
10.9 MB for a 216-board workspace, re-sent on every load and reconnect, with
warm loads paying the same as cold (no cache, no diff). At 10 Mbps that is
~10 s before the deep-linked board can render.
- server: /events?lite=1 sends metadata only ({...meta, bytes}); content ships
per board via the existing scoped GET /state. Clients without the flag keep
the full snapshot (old clients, share surface).
- server: sendJson() gzips/brotlis JSON bodies >=1KB when negotiated — a
single board's /state can be ~1 MB raw and compresses 4-8x.
- client: snapshot handling is a batched catalog merge (one sidebar render,
not one per board), content is fetched on select/deep-link and coalesced,
a newer ts drops stale cached content, deletions are swept.
- client: last-visit cache in localStorage (catalog + last rendered board,
<=1.5 MB, debounced) paints the previous screen BEFORE any network — the
identical-content render skip (#251) makes reconciliation flicker-free.
A/B, same 216-board/8.2MB seeded workspace, cold first-board render:
unthrottled 30 Mbps/40ms 10 Mbps/80ms
before 2.5 s 5.0 s 10.2 s
after 0.7 s 1.0 s 1.6 s (warm 0.8 s)
snapshot 8.9 MB -> 0.03 MB (~300x)
total wire 15.4 MB -> 2.3 MB
Offline probe: with the server killed, a reload still paints the last board
from the visit cache in ~0.6 s. e2e 190/190, unit 647/647 (+2 new).
…n-order first The snapshot array is hydration order (oldest first), so a reload without a deep-link auto-selected a stale, alphabetical-ish first board (the 'judo pane'). Default selection is now the most-recently-updated board; auto-selections no longer claim user intent (select() sets `wanted`), so the persisted --focus replay (#241) still overrides the default while an explicit hash stays king.
315d6d6 to
35e07e5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem (measured, not guessed)
Opening the viewer reloaded everything from scratch, every time: the SSE
snapshotevent serialized the full content of every board into one uncompressed frame — 10.9 MB for the real 216-board workspace — re-sent on every page load and every reconnect, with warm loads paying exactly what cold loads pay (no cache, no diff, no compression). On a 10 Mbps connection that's ~10 s before the deep-linked board can even start rendering.Baseline (deployed workspace, real boards; then A/B on an identical seeded 216-board/8.2 MB workspace):
Snapshot: 8.9 MB → 0.03 MB (~300×). Total cold wire: 15.4 MB → 2.3 MB.
What changed
/events?lite=1) — the snapshot now carries board metadata only ({project, agent, type, description, ts, bytes}); content ships on demand per board through the existing scopedGET /state, fetched on select/deep-link (coalesced, newer-ts wins). Old clients / the share surface keep the full-snapshot path untouched.sendJson()gzips/brotlis negotiated JSON ≥1 KB; a single board's/statecan be ~1 MB raw and compresses 4–8× (base64-heavy boards especially).localStorage(≤1.5 MB, debounced); the next visit paints before any network happens, then the snapshot reconciles — and fix(viewer): skip #diagram re-render on identical live updates (scroll-reset bug) #251's identical-content skip makes a clean reconcile zero-flicker, zero-scroll-reset. Offline probe: server killed, reload → the last board still paints in ~0.6 s.ts.Verification
/stategzip round-trip).loadingFinishedmisses open streams; network throttled viaNetwork.emulateNetworkConditions).Notes / follow-ups
Z_SYNC_FLUSH) is a clean follow-up if that ever matters.