Repository navigation
perf(plugin-detail,app-shell): the record page's related column loads counts first, then rows only for the cards in view, and never blocks the details - #27636
Conversation
Claude-Session: https://claude.ai/code/session_01L6bhgVDURMCgpQ3vtrchrM Co-authored-by: Claude <noreply@anthropic.com>
…first (#27056) Claude-Session: https://claude.ai/code/session_01L6bhgVDURMCgpQ3vtrchrM Co-authored-by: Claude <noreply@anthropic.com>
…ading order (#27056) Claude-Session: https://claude.ai/code/session_01L6bhgVDURMCgpQ3vtrchrM Co-authored-by: Claude <noreply@anthropic.com>
…ounts; exports for the drawer prefetch documented (#27056) The first count set settles in a transition, so a record read that lands at the same moment draws the details first. A store that already holds every count does not release a first set on its own, and a settled record draws its counts at once when the drawer steps back to it. Claude-Session: https://claude.ai/code/session_01L6bhgVDURMCgpQ3vtrchrM Co-authored-by: Claude <noreply@anthropic.com>
|
domain:ui#1 · os-sales · session_01L6bhgXU26UBEjd1KKHgNEx Contract reviewServed-tier: Face: ① Derived judgments
② Semver level
③ Boundary flagsDev report 6501278306: Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:ui#1 · os-sales · session_01L6bhgXU26UBEjd1KKHgNEx 席位核对 · ACCEPT · 2026-10-29T16:01ZHead-sha:
|
Reconciles with #27636 (objectstack#27056): RelatedList's compact branch keeps the payload adoption ahead of the in-view gate and reads the card context once; the record page prefetches the drawer's upcoming record (RecordDrawerUpcomingContext) instead of its own neighbours context, and skips the related-count warm-up where the adapter answers the payload. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhCjfxaYJQyy6tFjVr
Requested by Sales · project thread
Fixes #27056
Clause-②: yes (widening: three new exports on the published
@object-ui/plugin-detailmain entry, a public-surface addition; no accept set moves)Metadata-protocol: none
维护者速读
ui/packages/plugin-detail/src/useRelatedCounts.ts里的useRelatedCounts(一组计数、统一占位、在 transition 里放行卡片);②RelatedList.tsx的 compact 分支(rowsWanted、compactSelect、countAnswers);③app-shell/src/views/recordDrawerStack.tsx的PrefetchUpcomingRelatedCounts。ui/下跑pnpm exec vitest run packages/app-shell/src/views/RecordDetailView.relatedCountsFirst-27056.test.tsx。预期 2 个测试通过:6 个关联列表先一起发 6 个计数,之后只有视口内且有数的 2 张卡各取 3 行。What changes
The record page's related column (the column tiers, from 1024 px) loads in steps, and the details column never waits for it.
useRelatedCounts, new).RelatedCardStackcounts every card it places in one request set, sent beside the record read through the existing count store (RelatedCountStore, the store the tab badges read, invalidated by the data-change bus). Each probe asks for the total and one row'sid(top=1&select=idon the wire). Every card shows one placeholder (data-related-card-pending) until the whole set has answered, so there is one loading state. The faces then mount in a React transition: a record read that lands at the same moment renders the details first. A card counted at 0 folds to its title row and never reads rows. A failed count leaves its card to read for itself, as before.RelatedList, compact branch). A card reads at most three rows, in its inherited sort. It asks only forid, the name field (declared or derived) and one chip field. The chip field comes from the authoredrelatedListColumnswhen they are declared, else the object's status field, else its first date field. A card reads only once it has been in view (useSeenOnce, anIntersectionObserverlatch,rootMargin200 px below the fold). While the count answers a card, its header draws from that count. The folded 「动态」 card (timeline switched off) is closed until opened, and its feed is not mounted until then.recordDrawerStack). The drawer works out the record that ↑ / ↓ would show next. Once the current record has loaded,PrefetchUpcomingRelatedCountswarms that record's counts (relatedCountTargetsInover the page drawn now), so the next page's cards draw their counts from the store.useRelatedCountsreturns per-card states (pending/counted/uncounted). The card face is not part of that shape, so Record page v4: same-shaped related panels with no collapse, a persistent tab row under the header (the enlarged state is the active tab), Activity as the first panel and a tab, one top bar on every face, tiers by container width #27524 can read the same hook.Additive API:
prefetchRelatedCounts,relatedCountTargetsIn,relatedCountTargetOfand the typesRelatedCountTargetandRelatedCountsSourceare exported for the drawer prefetch.RecordPageColumnsalso takes an optionalrelatedFieldsOf, which lets the host pass the child field definitions it already holds so the counts read no schema first.Tests
app-shell/.../RecordDetailView.relatedCountsFirst-27056.test.tsxrenders the real page at 1440 px over a data source that holds every related read, for a record with 6 related lists:$top3,$select ['id','name','status']). Nothing is read for the 0-count card in view or for the 3 cards below the fold. That is 8 reads in total, in 2 round trips.plugin-detail/.../useRelatedCounts-27056.test.tsx(8 tests):relatedCountTargetsInwalk skips gated and bound nodesplugin-detail/.../RelatedList.compactLazy-27056.test.tsx:$top3,$skip0 with the projectionIntersectionObserverthe card reads at onceapp-shell/.../recordDrawerStack.prefetch-27056.test.tsx: the next record follows the step direction, and the prefetch fires only once the current record is ready and the object matches.RecordPageColumns.switches-27052.test.tsx: updated so that the folded 「动态」 card starts closed with nothing mounted, and opening it mounts the feed.Ablations, each run against the tests above:
stateOfforced touncounteduseSeenOnceforced totrueAcceptance notes
main(/batchtakes writes only), and adding one is a server and protocol change outside this card. The set is N$countprobes of one id each, all sent together before any answers, behind one loading state. On HotCRM they go out within 8 ms of each other, in parallel with the record read (waterfall below).mainsent 10, in the same 2 round trips. Each count carries one id, and each row read carries 3 projected rows. Onmaineach read carried 5 rows with up to 9 populated lookups. The trade-off is that a card's first rows land 260–380 ms after the record read (the last card's by 330–490 ms), where onmainthey landed before it. Meanwhile each card shows its count in its header.longtaskentries). The transition keeps the card faces out of that render. Without it, Acme measured 302–312 ms on this branch, where the faces had mounted just before the record landed.Verification
Budget (HotCRM, measured)
main(6892888)How it was measured
objectstack dev --fresh). Thecrm_accountrecord page has 9 related lists in the related column: contacts, contracts, quotes, child accounts, events, leads, opportunities, tasks and cases.vite build) ofmainat 6892888 and of this branch, served byvite preview, which proxies to the backend. Measured in headless Chromium at 1600×1000 (the three-column tier) and once at 1600×700.main/branch rounds plus the 700 px round give 12 opens per build.responseEndof the record read.maindetails ms (3 rounds · 700 px)Network waterfall, Acme, round 1 (ms from navigation; record read 178→465 on main, 122→316 on this PR)
main:crm_contacttop=5+ populate of 5 lookupscrm_contracttop=5+ populate of 7 lookupscrm_quotetop=5 select=id,quote_number,name,status,total_pricecrm_accounttop=5 select=id,account_number,name,type,owner_id populate=owner_idcrm_eventtop=5+ populate of 9 lookupscrm_leadtop=5+ populate of 9 lookupscrm_opportunitytop=5+ populate of 7 lookupscrm_tasktop=5+ populate of 9 lookupscrm_casetop=5+ populate of 8 lookupscrm_accounttop=1lookup labelThis PR. Details are interactive at 513 and headers draw at 494:
crm_contacttop=1 select=idcrm_contracttop=1 select=idcrm_quotetop=1 select=idcrm_accounttop=1 select=id(child accounts: 0)crm_eventtop=1 select=idcrm_leadtop=1 select=id(0)crm_opportunitytop=1 select=idcrm_tasktop=1 select=idcrm_casetop=1 select=idcrm_contacttop=3 select=id,full_name,title,created_atcrm_contracttop=3 select=id,contract_number,signed_by,statuscrm_quotetop=3 select=id,display_title,name,statuscrm_eventtop=3 select=id,subject,statuscrm_opportunitytop=3 select=id,name,stagecrm_tasktop=3 select=id,subject,statuscrm_casetop=3 select=id,display_title,subject,statusAt 1600×700 the
crm_taskandcrm_casecards sit below the fold, and their row reads are not sent.Local checks (head 5d85c50)
pnpm --dir ui exec vitest run packages/plugin-detail: 336 files passed (1 skipped), 3070 tests passed (18 skipped).app-shell: the 180 test files that referenceRecordDetailVieworrecordDrawerStackall passed (1357 tests). This is a declared narrowing: the full app-shell suite is 1880 files and runs longer than an hour here, so CI runs all of it.type-checkpassed for@object-ui/plugin-detailand@object-ui/app-shell, and@object-ui/plugin-detailbuilds.node scripts/pm/dispatch-gates.mjs --ran: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN. Three gates were run a second time, and the table records the second runs:vitest run scripts/had one 15 s test time out while a locked app-shell run held the box.check:skill-examplesexited 3 because the client dists were not built yet.check:type-check-debtexited 3 after queueing on the lock.All three exited 0 on the rerun.
Gate record (exit · command)
Generated by Claude Code