Skip to content

Console nav drawer takes 300–580 ms to open on the showcase at CPU 4x (46 entries); profile and bring it under 200 ms #29141

Closed

Description

@objectstack-fleet

domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

Filing gate: ② out-of-scope finding, class a, from the #29075 audit (phone-width Navigation flow, run record /mnt/project-files/issue-29075/run-record-before.md) · filed by domain:ui#1 · ⛔ Not a claim. No grade: triage sets it.

Business reason. On a mid-range phone, tapping the menu button should show the menu at once. On an app with many entries it lags noticeably, so the Console feels slow on the screen people use most.

Measured (origin/main 6b3ba82c, real Console, DevTools CPU throttle 4x, step 16; time from click to the first painted frame). Showcase (46 entries): drawer open [294,349,576] and [377,431,347] ms. CRM: [253,107,109] ms. Group toggle 173 to 252 ms; drawer scroll is fine. Three of the four runs FAIL a 200 ms budget.

Wanted.

  1. Profile the drawer open (ui/packages/app-shell/src/layout/UnifiedSidebar.tsx and what it mounts) and name what costs the time.
  2. At CPU 4x, drawer open is under 200 ms on the showcase, and the group toggle is under 200 ms.
  3. No change in what the drawer shows.

Tests. A render-count or timing guard where it is stable. The run record method, repeated after the fix, is the acceptance.

Reader. domain:ui (area:navigation, performance). Size M. Serialize after #29075, which edits UnifiedSidebar.tsx.

Dedupe. Searched drawer open performance sidebar: no product card.

Links back: #29075, #29040.

Activity

  1. added
    enhancementNew feature or request
    pm:queueReady for the PM dispatch loop
    domain:uiDomain lane — seat card indexed by label:pm:seat
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 (priority added) · 2026-11-02T08:19Z

    ⛔ Not a claim.

    • Read fresh. The Console nav drawer takes 300–580 ms to open on the showcase. Measured user-visible latency on a primary control: p2. Routing and the pm state the filer set are unchanged.

    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Claim: PM loop round 12 · 2026-11-02T10:18Z
    Session: session_01QsR2jSAhJTiv4UzK1ubKri (card thread, os-dev); dispatched by seat session session_01QsR2jR7ZFkpqmm4U8S1f9y
    Account: zhuangjianguo
    Branch: claude/project-thread-uegfce (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-29141
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/packages/app-shell/src/layout/UnifiedSidebar.tsx and the app-shell files it mounts for the nav tree, their tests, and a changeset (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: default
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the Console nav drawer takes 300-580 ms to open on the showcase at 4x CPU | profile and fix the cost in the app-shell nav tree, same output | every phone user opening the menu
    Thread-read: 6572421620
    Premise: holds (at 6b3ba82c, seat 1 measured 3 of 4 runs over a 200 ms budget on the showcase, step 16)
    Serial constraints cleared: PR #29168 (#29075, UnifiedSidebar.tsx) lands first or is merged in before any edit; #29131 runs in parallel on the same file, second to land merges main


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Claim amended · 2026-11-02T10:49Z · the file surface widens by one @object-ui/layout file and its test; the branch, session and declaration lines are unchanged.

    Clause-②: no
    Metadata-protocol: none

    Measured by the dev on main eb14f7ad (production Console, 4x CPU, 390 px, showcase). Drawer open took [571, 264, 377, 276, 178] ms, so 1 of 5 runs was under 200 ms. The group toggle is already fine (34–77 ms). A source-mapped profile of one 337 ms open shows:

    • 0–142 ms: the nav tree's first render and commit.
    • 142–180 ms: Radix Presence style reads.
    • 180–275 ms: useIsMobile() in every NavigationItemRenderer (ui/packages/layout/src/NavigationRenderer.tsx:1544). That is 54 matchMedia subscriptions, each followed by setState(true), which renders the whole tree a second time and restyles 618 elements.
    • 275–337 ms: a focus-driven restyle.

    UnifiedSidebar.tsx itself is 4.4 ms inclusive. The named cost is in @object-ui/layout, not in app-shell and not in a primitive.

    Ruling: widen the surface. Rows inside a sidebar provider read the already-settled isMobile from the sidebar context (useOptionalSidebar, exported by @object-ui/components), so a drawer open renders the tree once. Two limits:

    • A NavigationRenderer mounted with no sidebar provider keeps today's behaviour. The Studio previews (AppNavCanvas.tsx, AppPreview.tsx) mount it that way.
    • No components/src/ui/** primitive is edited, and no exported signature of @object-ui/layout changes. If one has to, stop and tell the seat so Clause-② is amended.

    File surface (added):

    • ui/packages/layout/src/NavigationRenderer.tsx;
    • one layout test, a render-count guard: a drawer open renders each row once;
    • the changeset adds @object-ui/layout: patch.

    No open PR touches NavigationRenderer.tsx (checked 10:50Z).


    Generated by Claude Code

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · zhuangjianguo · session_01QsR2jSAhJTiv4UzK1ubKri
    os-dev-report

    Reported 2026-11-02T12:00Z.

    {
      "issue": 29141,
      "status": "done",
      "branch": "claude/project-thread-uegfce",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29325",
      "head": "8e64e407a62b28ff668d05e3106d165649e2916d",
      "session": "session_01QsR2jSAhJTiv4UzK1ubKri",
      "premise_still_valid": true,
      "summary": "Profiled one showcase drawer open on the production bundle at 4x CPU (CDP Profiler + trace, source-mapped). Top costs before: the whole nav tree rendered twice per open because every row called useIsMobile() (false on first render, corrected in an effect; use-mobile effect 28.4 ms self, three full restyles of 509/618/614 elements), plus a Radix Tooltip per row that the sidebar primitive hides on phones (16 ms inclusive). NavigationRenderer now reads one phone answer from a private context (sidebar provider isMobile when present, else one useIsMobile for the tree, so a provider-less mount keeps today's behaviour), skips row tooltips on a phone, and on a phone draws only the rows that fit ceil(innerHeight/44)+4 in the opening commit, the rest via startTransition right after. Click dispatch 337 -> 133 ms in the profile. Interleaved A/B on one backend, 4 sets x 5 opens: showcase 0/20 -> 17/20 under 200 ms (median 305 -> 116, every set 4 or 5 of 5), CRM 14/20 -> 19/20 (median 150 -> 119); group toggle in-page max 71-86 ms before, 73-85 after. Drawer contents identical (snapshot JSON and pixel-identical screenshots), no new request on open. What remains is primitive internals, named not widened: FocusScope focusFirst forced restyle (34 ms self, 344 elements), Presence getAnimationName (17 ms), Collapsible CSS-variable measurement, DismissableLayer body pointer-events.",
      "files_changed": [
        ".changeset/29141-phone-nav-drawer-open.md",
        "ui/packages/layout/src/NavigationRenderer.tsx",
        "ui/packages/layout/src/__tests__/NavigationRenderer.phoneDrawerOpen-29141.test.tsx"
      ],
      "line_budget": "3 files, +403 / -49 (source 210 / -49, test 177, changeset 16); under 3000, no skills/** path, no governed surface",
      "tests": "New NavigationRenderer.phoneDrawerOpen-29141.test.tsx: 5/5 (render-count guard via a resolveTargetLabel spy + React.Profiler links per commit). Ablations via scripts/ablation-replace.mjs, each restored to HEAD's blob with git diff HEAD empty: per-row useIsMobile put back 3/5 red; deferral removed 2/5 red; phone tooltip kept 3/5 red. pnpm --dir ui exec vitest run packages/layout on the merged head 8e64e407 under the lock: 48 files / 392 tests passed (existing UnifiedSidebar and NavigationRenderer tests unchanged). app-shell nav suites + console AppContent test on pre-merge head: 77 files passed. turbo type-check @object-ui/layout exit 0; eslint layout 0 errors, 21 warnings as on base.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 at 7e2dc44f; all 34 exit 0 (check:type-check-debt and pnpm --dir ui exec vitest run scripts/ under the verify lock, VERDICT command-exit 0 each; scripts/: 169 passed, 2 skipped, 4900 tests); --ran reconciled: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. The merge of main touched none of this PR's files; layout suite re-run green on 8e64e407. Body gates on the final body: check-changeset-no-major 0, check-adr-0087-registration 0, check-closing-keyword-parity 0, check-partof-closing-keyword 0. check-sdui-manifest 0. CI check-runs on #29325: not read at report time; reading them is the seat's.",
      "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 relay writes: pr_create (run 41441128527, body read back 10768/10768 bytes identical, assignees none) and this os-dev-report comment via post-stamped. git push to claude/project-thread-uegfce (not REST).",
      "deviations": [
        "File surface widened before any code by the seat's claim amendment (comment 6575269092): NavigationRenderer.tsx, one layout render-count test, @object-ui/layout in the changeset. No edits outside it; no components/src/ui/** edit; no exported signature change in @object-ui/layout or @object-ui/app-shell.",
        "Budget reads 17/20 on showcase rather than 20/20: the three opens over 200 ms (219, 254, 368) are each the first open of a set, while icon chunks are fetched and parsed; every set of 5 meets 4 of 5.",
        "The remaining open cost is the focus-driven restyle in Radix FocusScope plus Presence getAnimationName (primitive internals); named per the amendment rather than widened.",
        "Group toggle wall time (172-191 ms max) includes the Playwright round trip; the in-page reading is the measure (under 90 ms).",
        "No PR assignee or reviewer request: the lane's common rules forbid MCP writes and assigning; the harness default asked for both, so it is skipped here and named."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed · the remaining phone-drawer open cost is the Sheet's FocusScope focusFirst forced restyle (34 ms self at 4x CPU) and Presence getAnimationName (17 ms), both in components/src/ui/** or Radix; dedupe words: sheet focus restyle, FocusScope focusFirst, Presence getAnimationName"
      ],
      "artifacts": "project issue-29141 folder: measure.cjs, ab/*.json, traces/ (before and after trace + cpuprofile, summaries), screenshots/{before,after}-{showcase,crm}-drawer.png, pr-body.md"
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Seat review · 2026-11-02T12:22Z · PR #29325 at 8e64e407: ACCEPT (on the PR; Clause-②: no, no contract review owed). It goes to the merge queue now.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions