Skip to content

Console perf: opening a record refetches the list 10×, repeats field-access ×4 and security/explain ×2, and the inbox bell polls every 10 s — dedupe in-flight requests, pin the list's query key, push instead of poll #27626

Open

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

Filed from the record-page epic #26676 as a sub-issue after the maintainer's 「别人的界面很丝滑,但是我们的界面总觉得卡顿,云端更卡」 and the seat's measurement (2026-10-29, console main 273137d227 in Vite dev mode against HotCRM on localhost:4001, Chrome, 1918 px). Ruling: 「立 A(ui 缺陷:列表不重取、同 URL 去重、消息中心停轮询)…两张卡,p1」. This card is the client-side waste; the server-side shape is #27627.

Measured (Resource Timing + PerformanceObserver longtask, buffer raised to 20 000)

Opening one HotCRM contact from the contacts list (?recordId=):

cold (first open after load) warm (second open)
requests until idle 45 23
drawer visible 310 ms 300 ms
last request end 440 ms (+ a PATCH sys_user_preference at 850 ms) 360 ms
long tasks 2 / 144 ms 3–4 / 280–400 ms

Pure waste inside those:

  • The contacts list query (GET /api/v1/data/crm_contact?…top=50…) is refetched 10 times between 309 and 517 ms of the cold open — one per arriving child-object metadata (/meta/object/crm_event, crm_event_attendee, crm_lead, crm_opportunity, crm_task, crm_case, crm_contract, crm_campaign_member, crm_quote — 9) plus one. Each metadata arrival re-renders the list host and its data hook refetches. Twenty hit the same defect (Stop side panel URL changes from re-rendering the record index twentyhq/twenty#27438 cut row renders per open from 244 to 4).
  • GET /api/v1/data/crm_contact/:id/field-access ×4, identical URL, in the same 50 ms, all 404 (consumers: ui/packages/plugin-detail/src/recordFieldAccess.tsx, plugin-form/src/recordFieldAccess.tsx, permissions/src/RecordFieldAccessScope.tsx, data-objectstack/src/index.ts).
  • POST /api/v1/security/explain ×2 at the same instant (plugin-detail/src/useRecordEditable.ts, plugin-detail/src/queueClaim.ts).
  • The inbox bell polls sys_inbox_message every 10 s and approvals/requests every 30 s (ui/packages/app-shell/src/hooks/sharedUserFeeds.ts: INBOX_POLL_MS = 10_000, APPROVALS_POLL_MS = 30_000, HIDDEN_POLL_MS = 60_000) while the page already holds two WebSocket connections; the measuring tab had made more than 110 000 requests over one working day. On the cloud deployment the same poll took 2.4–7.5 s per call under load, competing with the user's own navigation.

Why it matters more on the cloud: with ~0.5 s per request on huayan.objectos.app (see #27627 for the server side), every redundant request is half a second the user waits.

Target

  1. The list's query key is independent of ?recordId= and of child metadata arrivals. Opening, stepping and closing a record must not refetch the list (a render-count / request-count test: 0 list requests on open). The list host re-renders at most once per record change.
  2. In-flight request dedupe at the data layer: identical GETs (same URL) and identical POST security/explain bodies share one in-flight promise and one short-lived cache entry (per page life for metadata, per record for field-access / explain); a 404 field-access is cached as "no scope" for the record's life, not retried four times.
  3. Child metadata loads once: the first open of a record pulls each related object's metadata once per session (today: once per open on the cold path), cached in the same data layer.
  4. No polling while a push channel is open: the inbox bell and the approvals badge subscribe to the existing WebSocket feed; polling remains only as the fallback when the socket is down, at ≥ 60 s, and pauses in hidden tabs (today's HIDDEN_POLL_MS logic stays for the fallback).
  5. No metadata key; ⛔ packages/spec/**.

File surface

ui/packages/app-shell/src/views/objectViewListRenderer.tsx + the list data hook it uses, ui/packages/data-objectstack/src/index.ts (dedupe / cache), ui/packages/plugin-detail/src/recordFieldAccess.tsx, plugin-form/src/recordFieldAccess.tsx, permissions/src/RecordFieldAccessScope.tsx, plugin-detail/src/useRecordEditable.ts, plugin-detail/src/queueClaim.ts, ui/packages/app-shell/src/hooks/sharedUserFeeds.ts + useInboxBell.ts (push subscription), tests beside, changeset.

Acceptance

  1. Tests: opening a record from a list fires 0 list requests; the same URL in flight is fetched once; security/explain for the same record and user is posted once; child metadata is fetched once per session; with a socket open the bell makes no polling requests in 60 s, with the socket closed it polls at ≥ 60 s.
  2. A before / after request log on the same HotCRM contact (cold and warm), in the PR body, from the measurement recipe below.
  3. The epic seat re-measures in the console preview before ACCEPT (maintainer: 「车队开发完后你审核效果」).
Measurement recipe (browser console, on the list page)
performance.setResourceTimingBufferSize(20000); performance.clearResourceTimings();
const t0 = performance.now(); document.querySelector('a[data-testid="primary-field-link"]').click();
await new Promise(r => setTimeout(r, 7000));
const g = {}; for (const r of performance.getEntriesByType('resource').filter(r => r.initiatorType !== 'script')) {
  const k = r.name.replace(/^https?:\/\/[^/]+/, '').replace(/\?.*$/, '') + (r.name.includes('top=50') ? ' [LIST]' : '');
  (g[k] = g[k] || { n: 0, first: 1e9 }).n++; g[k].first = Math.min(g[k].first, Math.round(r.startTime - t0)); }
console.table(Object.entries(g).map(([u, v]) => ({ u, ...v })));

Activity

  1. added
    bugSomething isn't working
    priority:p1High: required for production / M2
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    on Oct 29, 2026
  2. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

    Claim: PM loop round 1
    Session: session_01Utp6CbDAucNyQ4UtzCJKyh
    Account: os-bill
    Branch: claude/project-thread-imv3an (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-27626
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: the card's own list — app-shell objectViewListRenderer.tsx + its list data hook, data-objectstack/src/index.ts (in-flight dedupe/cache), the three recordFieldAccess consumers (plugin-detail, plugin-form, permissions), plugin-detail useRecordEditable.ts / queueClaim.ts, app-shell sharedUserFeeds.ts + useInboxBell.ts; app-shell console/AppContent.tsx + console/appObjectScope.ts (amended 2026-10-30T02:16Z: measured cold open — ?recordId= turns the scope candidate off, routeReadsKey gains object, and the route slot is swapped for RouteMetadataSkeleton until the full /meta/object lands, unmounting ObjectView/ListView; the list route keeps its scope and the full registry is read beside it and merged in on arrival, so the record overlay still reads the full registry; ⛔ ObjectView.tsx (#27545)); tests beside; root .changeset/; ⛔ packages/** outside ui/; ⛔ useConsoleActionRuntime.tsx / RecordDetailView.tsx apiHandler (in-flight PR #27793) (stop on breach; explain in the report)
    Container & model: L, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier on fec60e00: no path-derived mandate), effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Responsibility: opening a record from a list fires 0 list requests; identical in-flight GETs and explain POSTs share one promise; child metadata once per session; the bell stops polling while the socket is open (fallback ≥60 s); before/after request log in the PR body; the epic seat re-measures in the console preview before ACCEPT; ⛔ any of #27627 (server shape)
    Thread-read: none
    Premise: holds
    Serial constraints cleared: maintainer 2026-10-30 01:47Z/01:49Z dispatch-now; in-flight: PR #27793 (excluded), seat 4 #27504 ui half (data-objectstack stamp, unpushed — additive seam)

    Dispatched 2026-10-30T01:54Z to a card thread of the seat's project; the order lives in the project. ⛔ The thread posts no claim and writes no assignee.


    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

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uipm:dispatchedpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions