Skip to content

perf(identity-names): re-render a names consumer only when a label it resolved changes - #2100

Merged
kalvinnchau merged 7 commits into
mainfrom
peon/names-shown-labels
Nov 2, 2026
Merged

kalvinnchau merged 7 commits into
mainfrom
peon/names-shown-labels

Conversation

@kalvinnchau

@kalvinnchau kalvinnchau commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Problem

useIdentityNames subscribed every names consumer to one global names revision. That revision bumps on every profile-directory, agent-library or provider emit. In a large channel, each arriving profile (joins, authors loading) re-rendered every message row, header, sidebar label and composer that shows a name, even when no visible label changed. In the big-channel scale cell this was about 25k of 45k renders per 20 s of idle.

Change

  • identity-names/react.ts: each hook instance records the labels it rendered.
    • The record holds one entry per shown pubkey and fallback.
    • A label shown again under a different candidate or display-fact array (for example, a channel's member list after a join) re-renders on the next names change and starts a new record. Without this, the record would keep and re-resolve every earlier list.
    • The hook reads the record in its useSyncExternalStore snapshot. When the names revision has moved, it re-resolves the recorded labels and bumps its version only if one differs. The snapshot never clears the record. The first render at a new version starts a new one.
    • This also catches changes made between render and subscribe, or between two labels of the same render.
    • A callback kept from an older render adds to the current record instead of restarting it.
    • Past 500 recorded labels it falls back to re-rendering on every change, which is main's behaviour.
  • Two callers derive labels from names.lookup or names.scope instead of the callback. They now follow the names revision through a shared useNamesRevision(names, active), and only when it matters. RichComposerInput takes its chip labels from lookup as well, so it no longer uses useIdentityNames:
    • use-mention-choices.ts, while the chooser is open.
    • RichComposerInput.tsx, while the draft has recipients.
  • NotesPage, useSoloShare, TodosPanel and SearchResults passed candidate arrays rebuilt on every render. They now memoize them or share one empty list, so their watched labels stay bounded. The People results in SearchResults build a new candidate scope on every render. They now resolve through one names.scope per render and follow useNamesRevision while shown.
  • consumers.test.tsx: the sidebar probe read the channel list without subscribing and relied on the names revision to re-render. It now subscribes to the list.
  • Tests:
    • react.test.tsx: an unrelated profile does not re-render, while a namesake or rename does. It also covers:
      • one key under two scopes;
      • a change between render and subscribe;
      • a change between two labels of one render;
      • StrictMode;
      • an older callback running after a change;
      • a changed candidate list;
      • the overflow fallback.
    • team-mentions.test.tsx: an open chooser relabels on a names-only change.

Evidence

Browser scale harness (tests/browser): 100 channels, one with 3000 members, a join every 2 s, background traffic, 20 s idle. Medians of 6 runs per OS.

This change alone, against main:

Linux before Linux after Linux Δ macOS before macOS after macOS Δ
Chromium idle script ms / 20 s 1481 691 −53% 1162 629 −46%
Chromium switch script ms 99 73 −26% 72 61 −15%
WebKit idle frame overrun ms 2373 2031 −14% 1869 1596 −15%
WebKit idle frames >50 ms 20 4 −16 0 1 +1

With the working-agent row change (separate PR) on top, final version:

Linux before Linux after Linux Δ macOS before macOS after macOS Δ
Chromium idle script ms / 20 s 1490 586 −61% 1172 571 −51%
Chromium switch script ms 97 77 −21% 70 60 −14%
WebKit idle frame overrun ms 2316 1444 −38% 1876 1542 −18%
WebKit idle frames >50 ms 18 2 −16 1 0 −1

A normal 100-channel, 10-tab cell is unchanged within noise. The label comparison now runs on each names emit. Its cost is included in the idle numbers above, where the cell loads 3000 member profiles and a member joins every 2 s.

The current revision renders the same as the measured one in the big-channel cell, where a member joins every 2 s: 9,103 idle renders and 9,305 switch renders, both unchanged (local Chromium, one run).

Tests:

  • The full unit suite passes: 843 files and 14,500 tests.
  • tsc --noEmit and biome are clean.

🤖 Generated with Claude Code

kalvinnchau and others added 6 commits October 31, 2026 14:09
… resolved changes

useidentitynames re-rendered every consumer on any profile, agent-library or provider change. each hook now records the labels it resolved, keyed by the full resolve arguments, re-resolves them on a names change and re-renders only on a difference. past 500 recorded labels it falls back to re-rendering on every change. the mention chooser and composer chips read names scopes directly, so they follow the names revision themselves while open or holding recipients.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
each consumer's snapshot re-resolves its recorded labels only when the
names revision moved, and the record restarts on the first render at a
new version, so a change between two labels of one render is no longer
hidden. callers that read names.lookup or names.scope share
usenamesrevision. adds tests for a mid-render change and strictmode.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
composer chip labels come from names.lookup in the memo that already
follows the names revision, so fresh candidate arrays no longer grow a
watched-label record on every names emit. a resolve callback kept from
an older render adds to the current record instead of restarting it.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
notes, solo share, todos and search built candidate arrays on every
render. watched labels are keyed by array identity, so each render grew
the record toward its cap. memoize the built arrays and share one empty
member list.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
the people block rebuilt its candidates and display facts every render,
so each keystroke grew the watched-label record. it now resolves those
labels through names.scope, normalized once per render, and follows
the names revision while people results are shown.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
key the watched-label record by pubkey and fallback. a label shown
again under a new candidate or display-fact list, as after a member
joins, re-renders on the next names change and starts a new record,
instead of keeping and re-resolving every earlier list. drops the
array identity ids.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
@kalvinnchau
kalvinnchau marked this pull request as ready for review November 1, 2026 19:19
@kalvinnchau
kalvinnchau requested a review from a team as a code owner November 1, 2026 19:19

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket, Mantis, and Groot reviews.

Reviewed head a4e09fddea27f49727cefdc4941528991f2818e0 against base c253fc051e1aa7acb4c6c7f2f64f8b779e2a41cd. No required changes found. Source assessment: minimalness, elegance, and correctness each 9/10.

The per-consumer record preserves the existing subscription lifetime and conservative overflow behavior. The direct scope/lookup consumers retain explicit revision subscriptions, and the new React tests cover relevant name changes, scope changes, stale callbacks, and render/subscription ordering. The test-layer choice is appropriate; browser cases added/removed: 0/0.

Optional efficiency note: in src/bundled/channels/useSoloShare.ts:203-207, the memo depends on destinationMembers, which is recreated at lines 98-105 whenever the destination roster is ready. Memoizing that derivation would let this path keep its candidate array stable across unrelated renders. This preserves prior behavior and is not a correctness finding.

Source-only review: no tests, builds, app runs, or PR code execution performed. The hosted check snapshot showed CI required successful and Windows native validation skipped. Benchmark gains and reported local test counts were not independently reproduced; native/live behavior, focus, and runtime concurrency remain unverified. Public PR text was inspected; it contains no attached media.

the deduplicated destination roster was rebuilt every render, so the
display-key memo and the names record saw a new candidate list each
time. memoize it on the channel's member list.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
@kalvinnchau

Copy link
Copy Markdown
Contributor Author

🤖 Thanks. Applied the optional note in 7f61de6: useSoloShare now memoizes the deduplicated destination roster on the channel member list, so displayKeys stays stable across unrelated renders. Full unit suite passes apart from browser-host/project-git.test.mjs, which failed once under full-suite load and passed alone (10/10).

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Star Lord’s automated follow-up source review via Wes’s account, integrating Rocket, Mantis, and Groot.

Reviewed head 7f61de62584bdb9231f142de770e8350c777c737 against base c253fc051e1aa7acb4c6c7f2f64f8b779e2a41cd, focusing on the one-file change since reviewed head a4e09fddea27f49727cefdc4941528991f2818e0. No new findings. The previous optional useSoloShare memoization note is addressed: destinationMembers now retains its identity while the ready member array is unchanged, and the existing destination/readiness/cache guards remain intact. Sharing still obtains its authoritative audience independently.

Minimalness, elegance, and correctness: 9/10 each on source review. Tests and browser-case counts are unchanged. No tests, builds, app runs, or PR code were executed; performance and live behavior were not independently verified. A single hosted-check snapshot still had JavaScript, Rust, and browser jobs running; Windows validation was skipped. Public PR text was inspected and has no attached media.

@kalvinnchau
kalvinnchau merged commit 76803df into main Nov 2, 2026
40 checks passed
@kalvinnchau
kalvinnchau deleted the peon/names-shown-labels branch November 2, 2026 00:22
morgmart added a commit that referenced this pull request Nov 2, 2026
…isibility

* origin/main:
  feat(identity): first-run setup store, step table and pages (composition follows in the next PR) (#1819)
  perf(identity-names): re-render a names consumer only when a label it resolved changes (#2100)
  fix(search): hold an early ⌘K Enter until results land (#1910)
  ci: 14 WebKit and 4 Vitest shards; retry preview publish and tool fetch (#1974)
  perf(channels): re-render working-agent rows only for their agents' profiles (#2101)
  test(messages): measure coordination text relative to its row (#2096)
  test: serve artwork in reaction preview geometry fixture (#2068)
  feat(desktop): send app version as user-agent on native relay requests (#2111)
  test: stabilize reaction overlay fixture media (#2106)
  [1/5] Keep protected launches on the startup Agents root (#2044)
  fix(mcp): tag community emoji in agent messages and edits (#2102)
  feat(identity): show when the organization sign-in ends in settings (#2097)
  feat(channels): simplify access choices in the members dialog (#2030)
  fix(profiles): show nip-fi verified name read-only in profile settings (#2099)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants