MOTION: find and fix janky/laggy animations across the app (trace every animation, fix at the primitive) #414

Open
opened 2026-09-29 08:21:32 +00:00 by kayg · 8 comments
Owner

Request (owner, 2026-09-29)

"in general i want a /codex-subagents looking out for janky/laggy animations in the app (basically review and then fix)"

Review, then fix

  1. Record a performance trace (Chrome DevTools Performance / Playwright tracing with screenshots, CPU 4x throttle) for every animated interaction: sheet open/close, sidebar collapse/expand and resize, segmented controls and view switches (Day/Week/Month/Year, the mini-calendar band), menus and submenus, popovers and previews, toasts (and the #405 ring), composer mode morph, search palette open/expand, Photos timeline scrub and zoom, Calendar scroll and zoom, drag interactions, the tab bar capsule.
  2. For each: frame times (dropped frames, long tasks > 50 ms), layout thrash (forced reflow, layout of large subtrees), paint areas, animated properties that are not transform/opacity, backdrop-filter cost, JS work on the animation frame.
  3. Write docs/perf/animation-review-.md with a table (interaction → p95 frame time → cause → fix) and fix each cause at the shared primitive (motion.ts spring, OverlaySurface, FloatingSurface, TabBar, TimeGrid…), not per screen.
  4. Target: 60 fps (≤ 16.7 ms p95 frames) on a mid-range laptop profile and on a phone profile (Playwright device emulation + 4x/6x throttle); reduced motion unchanged.
  5. Re-measure after the fixes; attach before/after traces and frame strips.
## Request (owner, 2026-09-29) "in general i want a /codex-subagents looking out for janky/laggy animations in the app (basically review and then fix)" ## Review, then fix 1. Record a performance trace (Chrome DevTools Performance / Playwright tracing with screenshots, CPU 4x throttle) for every animated interaction: sheet open/close, sidebar collapse/expand and resize, segmented controls and view switches (Day/Week/Month/Year, the mini-calendar band), menus and submenus, popovers and previews, toasts (and the #405 ring), composer mode morph, search palette open/expand, Photos timeline scrub and zoom, Calendar scroll and zoom, drag interactions, the tab bar capsule. 2. For each: frame times (dropped frames, long tasks > 50 ms), layout thrash (forced reflow, layout of large subtrees), paint areas, animated properties that are not transform/opacity, backdrop-filter cost, JS work on the animation frame. 3. Write docs/perf/animation-review-<date>.md with a table (interaction → p95 frame time → cause → fix) and fix each cause at the shared primitive (motion.ts spring, OverlaySurface, FloatingSurface, TabBar, TimeGrid…), not per screen. 4. Target: 60 fps (≤ 16.7 ms p95 frames) on a mid-range laptop profile and on a phone profile (Playwright device emulation + 4x/6x throttle); reduced motion unchanged. 5. Re-measure after the fixes; attach before/after traces and frame strips.
Author
Owner

Starting #414 on branch job/jank, based on dev at 1ad7b72895acaa85984f0f6d0748fdc02192d4f2. I have read CLAUDE.md, CONTEXT.md, the animation-related DESIGN.md sections, and the issue. I am mapping the shared animation primitives and setting up production-build traces before changing code.

Starting #414 on branch `job/jank`, based on `dev` at `1ad7b72895acaa85984f0f6d0748fdc02192d4f2`. I have read CLAUDE.md, CONTEXT.md, the animation-related DESIGN.md sections, and the issue. I am mapping the shared animation primitives and setting up production-build traces before changing code.
Author
Owner

Baseline finding — commit a19f00e1, production web build and real server on the quiet perf VM. In the 390×844 phone profile at 6× CPU throttle, Calendar's Week switch measured p95 66.6 ms, 17 frames over 16.7 ms, 2 long tasks over 50 ms (maximum 321 ms), 144.2 ms cumulative forced style/layout (117.9 ms maximum), and 63.1 ms CDP LayoutDuration. Day measured p95 50.0 ms, 49.7 ms cumulative forced style/layout, and 27.7 ms LayoutDuration.

The shared TimeGrid measure() currently writes --colw, reads two header getBoundingClientRect() values, then writes --colw again. This is a synchronous write/read/write sequence in the ResizeObserver path. It is the leading code-level explanation for the Calendar forced-layout cost; I will remove the forced read and compare the same trace after the fix.

The 390 px light and dark Playwright traces completed with 39 measured interactions each. Other before-trace artifacts are still running.

Baseline finding — commit a19f00e1, production web build and real server on the quiet perf VM. In the 390×844 phone profile at 6× CPU throttle, Calendar's Week switch measured p95 66.6 ms, 17 frames over 16.7 ms, 2 long tasks over 50 ms (maximum 321 ms), 144.2 ms cumulative forced style/layout (117.9 ms maximum), and 63.1 ms CDP LayoutDuration. Day measured p95 50.0 ms, 49.7 ms cumulative forced style/layout, and 27.7 ms LayoutDuration. The shared TimeGrid `measure()` currently writes `--colw`, reads two header `getBoundingClientRect()` values, then writes `--colw` again. This is a synchronous write/read/write sequence in the ResizeObserver path. It is the leading code-level explanation for the Calendar forced-layout cost; I will remove the forced read and compare the same trace after the fix. The 390 px light and dark Playwright traces completed with 39 measured interactions each. Other before-trace artifacts are still running.
Author
Owner

Merged dev finding, fixed — the first production run after the required dev merge raised 11 uncaught Cannot read properties of null (reading 'origin') errors while the runner switched Calendar routes. The new afterNavigate same-origin check in apps/web/src/lib/components/app-sidebar.svelte read navigation URLs without checking that both targets had URLs. I added the guards in commit 4fc739c6. The next 390 px production run completed 39 interactions in both light and dark with no uncaught page errors.

Merged dev finding, fixed — the first production run after the required dev merge raised 11 uncaught `Cannot read properties of null (reading 'origin')` errors while the runner switched Calendar routes. The new `afterNavigate` same-origin check in `apps/web/src/lib/components/app-sidebar.svelte` read navigation URLs without checking that both targets had URLs. I added the guards in commit `4fc739c6`. The next 390 px production run completed 39 interactions in both light and dark with no uncaught page errors.
Author
Owner

Performance findings for #414 (before shared primitive fixes): production captures cover 39 phone, 36 tablet, and 42 desktop interactions in both themes. The Calendar busy-week runner records 166.7 ms p95 for desktop pinch zoom and 266.6 ms p95 for horizontal fling at 4×; the phone pinch is 116.7 ms p95 at 6×, while phone horizontal and vertical scroll are 16.7–16.8 ms p95. The VM control page is also slow (32 fps desktop and 24 fps phone while resizing 210 plain boxes), so I will report the host-relative results as SLOW-only.

I found two shared layout causes to fix: TimeGrid.measure() writes --colw then reads adjacent header boxes synchronously inside ResizeObserver before writing the track again; OverlaySurface's sticky-title loop writes CSS variables for one heading before reading the next heading rect. The latter forces layout between heading reads while a sheet scrolls. The Calendar fixture helper also still searched Notes/Journal/ after dev moved Daily notes to the canonical flat Notes/ path; I updated the helper and kept its existing assertion. Commits: e9705a05, bd329706, 0fdf1b23.

Performance findings for #414 (before shared primitive fixes): production captures cover 39 phone, 36 tablet, and 42 desktop interactions in both themes. The Calendar busy-week runner records 166.7 ms p95 for desktop pinch zoom and 266.6 ms p95 for horizontal fling at 4×; the phone pinch is 116.7 ms p95 at 6×, while phone horizontal and vertical scroll are 16.7–16.8 ms p95. The VM control page is also slow (32 fps desktop and 24 fps phone while resizing 210 plain boxes), so I will report the host-relative results as SLOW-only. I found two shared layout causes to fix: TimeGrid.measure() writes --colw then reads adjacent header boxes synchronously inside ResizeObserver before writing the track again; OverlaySurface's sticky-title loop writes CSS variables for one heading before reading the next heading rect. The latter forces layout between heading reads while a sheet scrolls. The Calendar fixture helper also still searched Notes/Journal/ after dev moved Daily notes to the canonical flat Notes/ path; I updated the helper and kept its existing assertion. Commits: e9705a05, bd329706, 0fdf1b23.
Author
Owner

Progress (2026-09-29): The first full Calendar trace at 1440 px grew to 170,096,942 bytes and retained only the pinch start mark; the horizontal and vertical interaction trace metrics were missing. Phone and tablet marks were present. I removed the layer-snapshot category while keeping Paint, Layout, frame and user-mark events, added Layout/style duration totals, and added a one-profile capture option. Commit: 16badadf. I am rerunning the paired Calendar captures with the same recorder settings.

Progress (2026-09-29): The first full Calendar trace at 1440 px grew to 170,096,942 bytes and retained only the pinch start mark; the horizontal and vertical interaction trace metrics were missing. Phone and tablet marks were present. I removed the layer-snapshot category while keeping Paint, Layout, frame and user-mark events, added Layout/style duration totals, and added a one-profile capture option. Commit: `16badadf`. I am rerunning the paired Calendar captures with the same recorder settings.
Author
Owner

Calibrated Calendar result (same production fixture, trace categories, Playwright/Chromium, and CPU rates before and after): pinch p95 was 150.0 → 116.7 ms on phone, 100.0 → 100.1 ms on tablet, and 166.8 → 183.4 ms on desktop. Horizontal-fling p95 was 16.7 → 16.7 ms, 16.8 → 16.8 ms, and 349.9 → 266.7 ms. Vertical-scroll p95 was 16.8 → 16.7 ms, 16.7 → 16.7 ms, and 133.3 → 133.3 ms. All six start/end marks are present in each width's before and after Chrome trace. The plain-box control also missed frames (210-box resize p95: 50/33.4/33.4 ms before and 50/33.4/50 ms after, phone/tablet/desktop). The VM load averages at run start were 5.9/5.6/4.1 before and 1.2/4.3/4.5 after. The 60 fps target is not met on this VM; slow-only results do not block a merge under the owner rule.

Calibrated Calendar result (same production fixture, trace categories, Playwright/Chromium, and CPU rates before and after): pinch p95 was 150.0 → 116.7 ms on phone, 100.0 → 100.1 ms on tablet, and 166.8 → 183.4 ms on desktop. Horizontal-fling p95 was 16.7 → 16.7 ms, 16.8 → 16.8 ms, and 349.9 → 266.7 ms. Vertical-scroll p95 was 16.8 → 16.7 ms, 16.7 → 16.7 ms, and 133.3 → 133.3 ms. All six start/end marks are present in each width's before and after Chrome trace. The plain-box control also missed frames (210-box resize p95: 50/33.4/33.4 ms before and 50/33.4/50 ms after, phone/tablet/desktop). The VM load averages at run start were 5.9/5.6/4.1 before and 1.2/4.3/4.5 after. The 60 fps target is not met on this VM; slow-only results do not block a merge under the owner rule.
Author
Owner

Production animation review

Branch job/jank, head bea6c3e04005ba2ee2b33ab9d257d1f807e835f3 (pushed).

Report: docs/perf/animation-review-2026-09-29.md.

The production Chromium review covers 59 shell interactions and 9 Calendar interaction rows. The shell probe recorded 250 measurements across six width/theme pairs, with 59 distinct labels. The Calendar probe recorded CPU-throttled traces for three gestures at phone, tablet, and desktop widths. All 18 raw trace/metrics archives and 12 before/after frame strips are attached below.

Key measured changes: desktop paper sidebar resize rAF p95 fell from 649.9 ms to 283.4 ms, forced style/layout time fell from 4268.1 ms to 1595.8 ms, and Layout events fell from 50 to 37. Calendar desktop horizontal fling p95 fell from 349.9 ms to 266.7 ms. The report records remaining slow control results and their host-load context.

Fixes are in shared primitives: sticky heading bounds are read before state writes in OverlaySurface; TimeGrid removes the forced layout read/write cycle and uses the shared header/body track. The trace harness records bounded Chrome traces and per-interaction measures. Sidebar navigation also guards nullable URLs exposed by the merged dev branch.

Decision not specified in DESIGN: TimeGrid rounds measured width up to 1/64 CSS px so the shared grid track stays stable without repeated observer writes.

Known gaps: The full workspace clippy command was interrupted with exit code 130 just before the four-hour cap while it was compiling dependencies; it emitted no lint diagnostics before interruption. cargo test and the one-time adversarial server round were not run within the cap. The local adversarial server binary was not built.

Gate output

cargo fmt --check returned exit code 0 with empty stdout and stderr.

cargo clippy --all-targets -- -D warnings:

   Compiling phf_codegen v0.10.0

cargo test: Not run within the four-hour timebox.

bun run check:

Text sizes use shared role tokens.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/jank/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test:

Test Files  125 passed (125)
      Tests  800 passed (800)
   Start at  14:48:55
   Duration  193.48s (transform 54%, environment 18%, import 14%, tests 10%, setup 4%)

Attached trace and frame artifacts

# Production animation review Branch `job/jank`, head `bea6c3e04005ba2ee2b33ab9d257d1f807e835f3` (pushed). Report: [docs/perf/animation-review-2026-09-29.md](https://git.kayg.org/kayg/calternal/src/commit/bea6c3e04005ba2ee2b33ab9d257d1f807e835f3/docs/perf/animation-review-2026-09-29.md). The production Chromium review covers 59 shell interactions and 9 Calendar interaction rows. The shell probe recorded 250 measurements across six width/theme pairs, with 59 distinct labels. The Calendar probe recorded CPU-throttled traces for three gestures at phone, tablet, and desktop widths. All 18 raw trace/metrics archives and 12 before/after frame strips are attached below. Key measured changes: desktop paper sidebar resize rAF p95 fell from 649.9 ms to 283.4 ms, forced style/layout time fell from 4268.1 ms to 1595.8 ms, and Layout events fell from 50 to 37. Calendar desktop horizontal fling p95 fell from 349.9 ms to 266.7 ms. The report records remaining slow control results and their host-load context. Fixes are in shared primitives: sticky heading bounds are read before state writes in `OverlaySurface`; `TimeGrid` removes the forced layout read/write cycle and uses the shared header/body track. The trace harness records bounded Chrome traces and per-interaction measures. Sidebar navigation also guards nullable URLs exposed by the merged `dev` branch. **Decision not specified in DESIGN:** `TimeGrid` rounds measured width up to 1/64 CSS px so the shared grid track stays stable without repeated observer writes. **Known gaps:** The full workspace clippy command was interrupted with exit code 130 just before the four-hour cap while it was compiling dependencies; it emitted no lint diagnostics before interruption. `cargo test` and the one-time adversarial server round were not run within the cap. The local adversarial server binary was not built. ## Gate output `cargo fmt --check` returned exit code 0 with empty stdout and stderr. `cargo clippy --all-targets -- -D warnings`: ```text Compiling phf_codegen v0.10.0 ``` `cargo test`: Not run within the four-hour timebox. `bun run check`: ```text Text sizes use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/jank/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test`: ```text Test Files 125 passed (125) Tests 800 passed (800) Start at 14:48:55 Duration 193.48s (transform 54%, environment 18%, import 14%, tests 10%, setup 4%) ``` ## Attached trace and frame artifacts - [after-calendar-1440.tar.gz](https://git.kayg.org/attachments/18a8415b-7cf0-4681-a6bb-312a1d1c62d0) - [after-calendar-390.tar.gz](https://git.kayg.org/attachments/d0d4f0c0-c914-402d-b097-21991373756d) - [after-calendar-820.tar.gz](https://git.kayg.org/attachments/1764ef94-6b96-44d7-ae04-24288ed7eb85) - [after-shell-1440-paper.tar.gz](https://git.kayg.org/attachments/d17ea818-cd7d-4fdc-895b-e4edb7107b7f) - [after-shell-1440-tokyo-night.tar.gz](https://git.kayg.org/attachments/fdd65160-d8e1-46b1-8d19-b120bca0148e) - [after-shell-390-paper.tar.gz](https://git.kayg.org/attachments/e5afef6a-4586-42c1-a61e-5d39e943d918) - [after-shell-390-tokyo-night.tar.gz](https://git.kayg.org/attachments/b2c8ad3a-2fca-49ce-9753-1b22c179ac65) - [after-shell-820-paper.tar.gz](https://git.kayg.org/attachments/29a74a61-22c0-41b5-a36c-e37278160e5d) - [after-shell-820-tokyo-night.tar.gz](https://git.kayg.org/attachments/9cc8e78d-5755-4399-be8c-f127351a9655) - [before-calendar-1440.tar.gz](https://git.kayg.org/attachments/78c21204-f4d0-4f5b-86d8-f52c4c6c5e6f) - [before-calendar-390.tar.gz](https://git.kayg.org/attachments/f4e45a44-1b21-435e-b3d6-38ccf83befa9) - [before-calendar-820.tar.gz](https://git.kayg.org/attachments/339f4525-1d82-46d6-826b-37ab07eab729) - [before-shell-1440-paper.tar.gz](https://git.kayg.org/attachments/2d9bafb1-2b40-4e86-a63e-5b8aafd1bbf4) - [before-shell-1440-tokyo-night.tar.gz](https://git.kayg.org/attachments/4f360c20-ecac-47b8-a40f-5e87c52e4868) - [before-shell-390-paper.tar.gz](https://git.kayg.org/attachments/2825d986-b73c-4b31-ac15-080496623c22) - [before-shell-390-tokyo-night.tar.gz](https://git.kayg.org/attachments/5550efac-4bbb-4ee9-bf3c-e01e8b4dff49) - [before-shell-820-paper.tar.gz](https://git.kayg.org/attachments/13a2128c-d39a-4c44-a4c0-b7aa83facdf7) - [before-shell-820-tokyo-night.tar.gz](https://git.kayg.org/attachments/ac31f37c-5289-410f-9003-aa8c507c91ed) - [390-paper-transition-frames.png](https://git.kayg.org/attachments/4d64d833-7f35-40e0-92ae-730cdfbf86cf) - [390-tokyo-night-transition-frames.png](https://git.kayg.org/attachments/0496a066-81af-4f91-bf6e-cd2ea081d6ea) - [820-paper-transition-frames.png](https://git.kayg.org/attachments/46499564-836e-4cb0-b375-9f83c9654199) - [820-tokyo-night-transition-frames.png](https://git.kayg.org/attachments/2012827a-f40a-4184-a625-dad9ec857dec) - [1440-paper-transition-frames.png](https://git.kayg.org/attachments/65626a34-1627-418c-9153-615d1f61cd16) - [1440-tokyo-night-transition-frames.png](https://git.kayg.org/attachments/910a1acc-3eed-4faf-b5a5-dabecfee724b) - [390-paper-transition-frames.png](https://git.kayg.org/attachments/adfa4f86-8c69-4422-ba13-724e3d4cd693) - [390-tokyo-night-transition-frames.png](https://git.kayg.org/attachments/373d80a9-6337-4182-bf7e-e1558e3d32df) - [820-paper-transition-frames.png](https://git.kayg.org/attachments/9bd9015c-6343-40b3-b7da-8dbf5fedc036) - [820-tokyo-night-transition-frames.png](https://git.kayg.org/attachments/fa44cbb6-4b3b-485d-8439-2e5050fe72fb) - [1440-paper-transition-frames.png](https://git.kayg.org/attachments/e6bf17ff-f903-4922-9f90-5ed28ecc523e) - [1440-tokyo-night-transition-frames.png](https://git.kayg.org/attachments/4e2a064f-cec6-4fe2-9746-6b5eb9dc8403)
Author
Owner

Audit against origin/dev: the animation report records the first review and shared-primitive fixes, but the 60 fps target has not been re-measured on the quiet perf VM. The issue history and queue both leave that re-measure outstanding, so this issue remains open.

Audit against origin/dev: the animation report records the first review and shared-primitive fixes, but the 60 fps target has not been re-measured on the quiet perf VM. The issue history and queue both leave that re-measure outstanding, so this issue remains open.
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#414
No description provided.