Calendar hover previews are laggy while the pointer moves; reuse one card, cheap pointer path, hover intent before opening #608

Open
opened 2026-10-01 09:38:55 +00:00 by kayg · 5 comments
Owner

Owner report (2026-10-01)

"In calendar, moving around the cursor resulting in previews every second is very laggy. Please optimise that path and also wait slightly more before showing the hover preview (does not apply to tooltips). If it can be fixed without adding a delay - way better."
Fix:

  1. Profile the hover path (Chrome trace while sweeping the pointer across a busy Week view): find the cost per preview open (the layout read of anchors, mounting the full ItemPreview each time, attachment thumbnail requests, reactive invalidation of the whole grid on hover state, the backdrop-filter of the card).
  2. Make it cheap:
    • mount one preview card and reuse it (swap its content; no remount per item);
    • avoid layout reads on pointermove (cache item rects per layout pass);
    • hover state must not invalidate the grid (scope it to the card);
    • prefetch preview data only for the item under the pointer, after intent.
  3. Hover intent (Apple/macOS style): open the preview only when the pointer rests on an item (velocity below a threshold for ~150–250 ms; tune and cite). While the pointer is moving across items, do not open previews. Once a preview is open, moving to a neighbouring item swaps it without delay (as warm tooltips do). Tooltips keep their own timing.
    Budget: sweeping the pointer across 50 items in 2 s causes zero long tasks > 50 ms and INP ≤ 100 ms; the preview opens ≤ 16 ms after intent fires. Measure before/after on the perf VM (#563 harness).
    Test: an e2e sweep (Playwright mouse moves) asserts no preview opens during a fast sweep, and a preview opens after a rest. A trace-based perf profile is added to bench/.
## Owner report (2026-10-01) "In calendar, moving around the cursor resulting in previews every second is very laggy. Please optimise that path and also wait slightly more before showing the hover preview (does not apply to tooltips). If it can be fixed without adding a delay - way better." **Fix:** 1. **Profile the hover path** (Chrome trace while sweeping the pointer across a busy Week view): find the cost per preview open (the layout read of anchors, mounting the full ItemPreview each time, attachment thumbnail requests, reactive invalidation of the whole grid on hover state, the backdrop-filter of the card). 2. **Make it cheap:** - mount one preview card and **reuse** it (swap its content; no remount per item); - avoid layout reads on pointermove (cache item rects per layout pass); - hover state must not invalidate the grid (scope it to the card); - prefetch preview data only for the item under the pointer, after intent. 3. **Hover intent (Apple/macOS style):** open the preview only when the pointer **rests** on an item (velocity below a threshold for ~150–250 ms; tune and cite). While the pointer is moving across items, do not open previews. Once a preview is open, moving to a neighbouring item swaps it without delay (as warm tooltips do). Tooltips keep their own timing. **Budget:** sweeping the pointer across 50 items in 2 s causes zero long tasks > 50 ms and INP ≤ 100 ms; the preview opens ≤ 16 ms after intent fires. Measure before/after on the perf VM (#563 harness). **Test:** an e2e sweep (Playwright mouse moves) asserts no preview opens during a fast sweep, and a preview opens after a rest. A trace-based perf profile is added to `bench/`.
Author
Owner

Starting #608 on branch job/calcard-series. Branch base SHA: 6527b9ef9b5a9a1ecbceedc0a01b7fd36f8c5961. Current head after the completed #628 slice: 8ee6a0a34e6f6c8b0e930c9e68f9d1d466b02cac. I am profiling the production Week hover path with a real 50-item pointer sweep, then I will add the rest-intent e2e and trace profile.

Starting #608 on branch `job/calcard-series`. Branch base SHA: `6527b9ef9b5a9a1ecbceedc0a01b7fd36f8c5961`. Current head after the completed #628 slice: `8ee6a0a34e6f6c8b0e930c9e68f9d1d466b02cac`. I am profiling the production Week hover path with a real 50-item pointer sweep, then I will add the rest-intent e2e and trace profile.
Author
Owner

Profile finding for #608 (local Chrome, quick 56-Log fixture): the baseline hover path waits 320 ms before its first card and 60 ms before swapping to a neighboring item. Ten opens measured p50 399.8 ms / p95 415.9 ms; ten warm swaps measured p50 62.4 ms / p95 63.8 ms. A real CDP pointer sweep produced 50/50 block pointerovers in 2,711.5 ms, with no preview opening during the sweep. Its trace contains 9 long tasks over 50 ms (maximum 106 ms) and a 320 ms Event Timing sample. Layout/style events were counted in the attached trace; the pointer handlers contain no item-rectangle read. This is a local baseline, not the final 200-Log profile. Trace: artifacts/calendar-hover-608/calendar-hover-608.chrome-trace.json. The sweep's CDP acknowledgement time contributes to the measured 2.71 s; I am correcting the benchmark so it separates input dispatch overhead from browser event work before comparing the product change.

Profile finding for #608 (local Chrome, quick 56-Log fixture): the baseline hover path waits 320 ms before its first card and 60 ms before swapping to a neighboring item. Ten opens measured p50 399.8 ms / p95 415.9 ms; ten warm swaps measured p50 62.4 ms / p95 63.8 ms. A real CDP pointer sweep produced 50/50 block pointerovers in 2,711.5 ms, with no preview opening during the sweep. Its trace contains 9 long tasks over 50 ms (maximum 106 ms) and a 320 ms Event Timing sample. Layout/style events were counted in the attached trace; the pointer handlers contain no item-rectangle read. This is a local baseline, not the final 200-Log profile. Trace: artifacts/calendar-hover-608/calendar-hover-608.chrome-trace.json. The sweep's CDP acknowledgement time contributes to the measured 2.71 s; I am correcting the benchmark so it separates input dispatch overhead from browser event work before comparing the product change.
Author
Owner

#608 full busy-week baseline (200 Journal entries / 100 Photos), measured on the perf VM under /root/perf.lock at load average [0.41, 0.77, 0.43] before and [1.78, 1.10, 0.57] after:

  • 50 unique unobscured Calendar blocks received pointerover in 2.98 s under the Playwright/CDP controller; the preview stayed closed throughout.
  • Old hover behavior opened at p50/p95 329.2/346.2 ms after pointer entry; a warm neighbor swap measured 62.3/70.2 ms.
  • The Chrome trace had 154 pointer dispatches during the sweep. Handler duration was p50 0.157 ms, p95 0.317 ms, max 0.603 ms; 57 Layout events occurred in the sweep and none overlapped a pointer dispatch.

A full-fixture rerun exposed that the initial DOM probe also counted 100 asynchronous photo-thumbnail load mutations inside .file-thumb. I narrowed the probe to report thumbnail mutations separately and fail only on Calendar grid content mutations. This is test instrumentation; the trace shows no layout read inside pointer handlers. CDP/Playwright controller time includes roughly 1 second of per-move acknowledgement overhead, so I am keeping that separate from browser handler timings.

#608 full busy-week baseline (200 Journal entries / 100 Photos), measured on the perf VM under `/root/perf.lock` at load average `[0.41, 0.77, 0.43]` before and `[1.78, 1.10, 0.57]` after: - 50 unique unobscured Calendar blocks received pointerover in 2.98 s under the Playwright/CDP controller; the preview stayed closed throughout. - Old hover behavior opened at p50/p95 329.2/346.2 ms after pointer entry; a warm neighbor swap measured 62.3/70.2 ms. - The Chrome trace had 154 pointer dispatches during the sweep. Handler duration was p50 0.157 ms, p95 0.317 ms, max 0.603 ms; 57 Layout events occurred in the sweep and none overlapped a pointer dispatch. A full-fixture rerun exposed that the initial DOM probe also counted 100 asynchronous photo-thumbnail load mutations inside `.file-thumb`. I narrowed the probe to report thumbnail mutations separately and fail only on Calendar grid content mutations. This is test instrumentation; the trace shows no layout read inside pointer handlers. CDP/Playwright controller time includes roughly 1 second of per-move acknowledgement overhead, so I am keeping that separate from browser handler timings.
Author
Owner

Post-change quick Calendar hover profile (56 real Journal entries), local shared host; load average moved from [14.18, 16.77, 18.51] to [16.79, 17.04, 18.50]:

  • The controller delivered all 50 unique block targets, but the sweep took 7.17 s in Chrome / 7.21 s in Playwright. The slow controller path opened one preview after an item rested; the fast-sweep assertion did not apply.
  • There were 46 long tasks over 50 ms (max 986 ms), and max Event Timing was 1,960 ms. This run is not comparable to the perf-VM baseline because the local host was heavily loaded.
  • No Calendar grid DOM mutations occurred. The trace recorded 150 pointer dispatches; pointer-handler p50/p95/max was 0.222/3.154/15.813 ms. One Layout event overlapped a pointer dispatch on this loaded host.
  • Rest-open samples were p50/p95 320.5/380 ms from pointer entry, with 18.3 ms after the 200 ms intent. Warm neighbor swaps were p50/p95 4.6/12.7 ms; the ItemPreview article remained mounted.

The one preview during this run followed a slow controller rest under host contention. A separate low-load perf-VM quick profile on this build recorded 0 previews during its 50-item sweep, zero long tasks, max Event Timing 200 ms, and warm swaps p95 2.4 ms. I am running the full 200-Log/100-Photo post profile locally because the perf-VM lock remains occupied.

Post-change quick Calendar hover profile (56 real Journal entries), local shared host; load average moved from `[14.18, 16.77, 18.51]` to `[16.79, 17.04, 18.50]`: - The controller delivered all 50 unique block targets, but the sweep took 7.17 s in Chrome / 7.21 s in Playwright. The slow controller path opened one preview after an item rested; the fast-sweep assertion did not apply. - There were 46 long tasks over 50 ms (max 986 ms), and max Event Timing was 1,960 ms. This run is not comparable to the perf-VM baseline because the local host was heavily loaded. - No Calendar grid DOM mutations occurred. The trace recorded 150 pointer dispatches; pointer-handler p50/p95/max was 0.222/3.154/15.813 ms. One Layout event overlapped a pointer dispatch on this loaded host. - Rest-open samples were p50/p95 320.5/380 ms from pointer entry, with 18.3 ms after the 200 ms intent. Warm neighbor swaps were p50/p95 4.6/12.7 ms; the `ItemPreview` article remained mounted. The one preview during this run followed a slow controller rest under host contention. A separate low-load perf-VM quick profile on this build recorded 0 previews during its 50-item sweep, zero long tasks, max Event Timing 200 ms, and warm swaps p95 2.4 ms. I am running the full 200-Log/100-Photo post profile locally because the perf-VM lock remains occupied.
Author
Owner

#608 implementation report — branch job/calcard-series

Commits: 25593968e (rest intent and warm card reuse) and 3317795e2 (sweep regression assertions and profile record).

The hover card now waits for a 200 ms rest with 2 px jitter / 0.06 px/ms motion thresholds. A settled card stays mounted when its item changes. Preview data is resolved only after intent, and pointer movement does not read layout or write Calendar grid content. The e2e sweeps 50 distinct blocks, verifies no preview opens and no grid content changes, then verifies rest-open and warm swaps. It captures six 3× images at 390/820/1440 px in light/dark.

Final quick local e2e: Calendar hover #608: swept 50 blocks in 3646.6ms; preview metrics were cold_open_ms p50/p95=274.8/306.3, intent_to_open_ms=29.1, warm_swap_ms p50/p95=4/7.7. The sweep opened no preview, changed no Calendar content, had 18 Long Tasks over 50 ms (136 ms max), and had no Layout event overlapping a pointer dispatch. The e2e passed.

Performance profile: the 200-Log/100-Photo before run on the perf VM took 2,977 ms for 50 inputs and had 19 Long Tasks (108 ms max). The perf VM quick after run used 56 Logs, took 2,454 ms, and had zero Long Tasks; its intent-to-open was 13.9 ms and warm swap was 1.9/2.4 ms p50/p95. The full after run was local under high load and is not comparable: 6,083 ms sweep, 40 Long Tasks (223 ms max), zero grid content changes. The 2,000 ms budget and ≤100 ms Event Timing target were not met by the measured after runs; the report records this limit and the perf VM lock constraint in docs/perf/runs/2026-10-01-calendar-hover-608.md.

Screenshots and trace are in ignored artifacts/calendar-hover-608/. fj issue comment has no attachment option, so the files remain in the worktree rather than being uploaded.

Shared web check/test gates will run after the remaining issue slices. No push, deploy, or merge was performed.

#608 implementation report — branch `job/calcard-series` Commits: `25593968e` (rest intent and warm card reuse) and `3317795e2` (sweep regression assertions and profile record). The hover card now waits for a 200 ms rest with 2 px jitter / 0.06 px/ms motion thresholds. A settled card stays mounted when its item changes. Preview data is resolved only after intent, and pointer movement does not read layout or write Calendar grid content. The e2e sweeps 50 distinct blocks, verifies no preview opens and no grid content changes, then verifies rest-open and warm swaps. It captures six 3× images at 390/820/1440 px in light/dark. Final quick local e2e: `Calendar hover #608: swept 50 blocks in 3646.6ms`; preview metrics were `cold_open_ms p50/p95=274.8/306.3`, `intent_to_open_ms=29.1`, `warm_swap_ms p50/p95=4/7.7`. The sweep opened no preview, changed no Calendar content, had 18 Long Tasks over 50 ms (136 ms max), and had no Layout event overlapping a pointer dispatch. The e2e passed. Performance profile: the 200-Log/100-Photo before run on the perf VM took 2,977 ms for 50 inputs and had 19 Long Tasks (108 ms max). The perf VM quick after run used 56 Logs, took 2,454 ms, and had zero Long Tasks; its intent-to-open was 13.9 ms and warm swap was 1.9/2.4 ms p50/p95. The full after run was local under high load and is not comparable: 6,083 ms sweep, 40 Long Tasks (223 ms max), zero grid content changes. The 2,000 ms budget and ≤100 ms Event Timing target were not met by the measured after runs; the report records this limit and the perf VM lock constraint in `docs/perf/runs/2026-10-01-calendar-hover-608.md`. Screenshots and trace are in ignored `artifacts/calendar-hover-608/`. `fj issue comment` has no attachment option, so the files remain in the worktree rather than being uploaded. Shared web check/test gates will run after the remaining issue slices. No push, deploy, or merge was performed.
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
kayg/calternal#608
No description provided.