Skip to content

docs(perf): 2026-04 speed & functionality review#341

Open
spe1020 wants to merge 1 commit intozapcooking:mainfrom
spe1020:docs/perf-2026-04-review
Open

docs(perf): 2026-04 speed & functionality review#341
spe1020 wants to merge 1 commit intozapcooking:mainfrom
spe1020:docs/perf-2026-04-review

Conversation

@spe1020
Copy link
Copy Markdown
Contributor

@spe1020 spe1020 commented Apr 25, 2026

Summary

Investigation-only report covering toggle crispness on the Community feed and recipe pages, the relay strategy across the codebase, and the reaction button architecture (without regressing the count-correctness work in #321/#323). No production code changes — docs/perf/2026-04-review.md is the only file added.

Top findings (full detail in the report)

  • Feed tab remountroutes/community/+page.svelte:120 increments feedKey and :379 wraps the feed in {#key feedKey}. Every tab click destroys and remounts FoodstrFeedOptimized plus all its caches and subscriptions. Switching to a prop-driven filterMode is the biggest single perceived-latency win.
  • batchFetchEngagement imported but never called in FoodstrFeedOptimized.svelte:91. Five per-card children fan out individual fetchEngagement calls; the engagement-cache singleton dedup-protects correctness but the subscription count multiplies needlessly.
  • NIP-65 fully implemented for read, ignored on write. relayListCache.ts parses kind:10002 with read/write markers and SWR caching; publishQueue.ts routes through four hardcoded modes and never consults the cache. Replies, mentions, reactions, and zaps don't reach the recipient's published inbox.
  • members.zap.cooking is dead config. Referenced in tiers.ts and relays/relaySets.ts:42; the members mode in nostr.ts:140-176 exists; no active read or write call site targets it.
  • Comment toggle uses a fragile DOM bridge. NoteTotalComments.svelte:23-32 does document.querySelector('[data-event-id=...]').closest('.inline-comments').dispatchEvent(new CustomEvent('toggleComments')). Silent failure on any DOM-structure change. Subscription is also lazy at CommentThread.svelte:81 → 500ms+ first-paint stall.
  • ReactionPills {#each} is unkeyed. :113 iterates visibleGroups (sliced derived array, new ref every tick) without (group.emoji). Pills remount on every count update.
  • Minor NIP-25 e-tag drift at publishReaction.ts:50-59. ['e', id, '', 'reply'] puts the NIP-10 marker in what NIP-25 reserves for the author hint.
  • No reaction undo path. Kind:5 deletions only exist for recipes; reactions are one-way. Flagging now so it isn't bolted on without spec compliance later.
  • Six different timeout constants across six files, none abortable. No AbortController in the relay path.

What's in the report

  1. Executive summary (10 bullets)
  2. Section per scope area (toggles, relays, reactions) with file:line citations
  3. R1–R13 prioritized recommendations tagged with impact (H/M/L), risk (H/M/L), and effort
  4. Proposed 5-stage implementation order — Stage 1 measurement, Stage 2 the biggest felt-latency wins (R1+R2+R4), Stage 3 papercuts, Stage 4 relay strategy, Stage 5 render quality
  5. Open Questions — including: the perf/feed-measurement branch the original prompt referenced doesn't exist on origin (closest match: perf/following-feed-optimization)

Implementation footprint if we land just one follow-up PR

R1 (feed remount) + R2 (batch engagement) + R4 (pill key) in one change. ~1 dev-day, biggest felt wins, zero risk surface against the PR #321/#323 count-correctness work.

Test plan

  • Markdown renders correctly on GitHub
  • Reviewers agree on the implementation order in §6 before any follow-up PR

🤖 Generated with Claude Code

Investigation-only report covering toggle crispness on the Community
feed and recipe pages, the relay strategy across the codebase, and the
reaction button architecture. No production code changes.

Findings highlights:
- Feed tab switching destroys and remounts FoodstrFeedOptimized via
  {#key feedKey} — biggest single perceived-latency win is making this
  prop-driven.
- batchFetchEngagement is imported but never called from the feed; per-
  card sub fan-out is collapsing into the engagement cache singleton at
  read time but costs N concurrent subscriptions.
- NIP-65 (kind:10002) is fully implemented for read but ignored on
  write — replies/mentions/reactions don't reach the recipient inbox.
- members.zap.cooking is referenced in tiers and relay sets but no
  active call site uses it.
- Comment toggle on feed cells uses a fragile DOM-event bridge
  (querySelector + closest) that silently fails on DOM changes.
- ReactionPills {#each} lacks an explicit key; pills remount on count
  updates.
- Reactions are one-way (no NIP-09 undo). Spec compliance check finds
  a minor e-tag shape drift in publishReaction.

Report ends with prioritized recommendations (R1–R13) and a proposed
five-stage implementation order. Open Questions flag the missing
perf/feed-measurement branch the prompt referenced and several product
decisions that should land before Stage 4 relay work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@spe1020
Copy link
Copy Markdown
Contributor Author

spe1020 commented Apr 25, 2026

@copilot resolve the merge conflicts in this pull request

spe1020 added a commit that referenced this pull request Apr 26, 2026
* perf(feed): batch engagement for anon viewers too (R2)

Closes the last open recommendation from the perf review docs in
PR #341 (R1 / R4 already shipped via #344 and #342). The batch
helper itself is wired in at two sites — the rendered-events
preload and the visibility-update reactive — but both gate on
`$userPublickey`, which means signed-in users get batched fetches
while anonymous viewers fall back to the full per-card fan-out:

  NoteTotalLikes      → fetchEngagement(eventId)
  NoteTotalComments   → fetchEngagement(eventId)
  NoteRepost          → fetchEngagement(eventId)
  NoteTotalZaps       → fetchEngagement(eventId)
  ReactionPills       → fetchEngagement(eventId)

That's up to 5× the active subscription churn per card, multiplied
across a 30-event feed window, on every cold paint and every newly-
rendered card.

The gate isn't doing anything useful: `batchFetchEngagement`'s
server-counts API doesn't require auth, and the NDK fallback
subscription only uses `userPublickey` to flag userReacted /
userReposted — which already correctly stay false for anonymous
users either way. The per-card children themselves don't gate on
sign-in; only the batch did.

Drop the gate on both reactives so anonymous viewers get the same
batched preload + visibility refresh signed-in users already
benefit from. The 5-minute freshness gate inside
batchFetchEngagement preempts the subsequent per-card fetches
just like it does for the signed-in path.

No behavior change for signed-in viewers (gate was always passing
for them). Anonymous viewers see fewer concurrent NDK subs during
feed scroll. Counts and groups continue to populate identically;
no impact to the count-correctness work in #321/#323.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: spe1020 <sethsager@Seths-MacBook-Air.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
spe1020 added a commit that referenced this pull request Apr 26, 2026
* feat(relay): NIP-65 inbox routing for tagged-user events (R9)

Per NIP-65, when an event tags users via p tags (replies, mentions,
reactions, comments), clients SHOULD include those users' read
relays in the publish target so the events actually land where the
recipients read them. Today none of the publish paths consult
relayListCache for this, even though the cache is fully wired for
read flows.

Adds src/lib/nip65Routing.ts as the single source of truth:

- getRecipientInboxRelays(event): pulls non-self 64-hex pubkeys from
  the event's p tags, looks them up via the existing relayListCache
  (SWR + IndexedDB), bounded by INBOX_LOOKUP_TIMEOUT_MS (1.5s) so a
  hung discovery relay can't stall the publish path.
- unionInboxRelayUrls(event, baseUrls): unions caller-supplied base
  URLs with inbox URLs, dedupes, caps at MAX_PUBLISH_RELAYS (16) so
  a heavily p-tagged post can't fan out unbounded.
- buildInboxAwareRelaySet(opts): convenience for direct publishers
  that want an NDKRelaySet — falls back to the NDK pool's relays
  when no base set is supplied (matches the prior event.publish()
  no-args default), with inbox URLs added on top.

Wired into three publish paths:

1. publishQueue.attemptPublish — refactors the four-mode branched
   logic into a unified URL-collection step. relayMode (garden /
   pantry / garden-pantry / all) still picks the base relays
   exactly as today; inbox URLs are layered on. User intent takes
   precedence: when the cap kicks in, base relays survive and inbox
   URLs may be dropped.
2. publishReaction (kind 7): replaces reactionEvent.publish() (NDK
   default) with a publish to the inbox-aware set. Reaction now
   reaches the reacted-to author's read relays.
3. comments/postComment (kind 1 / 1111): both signing strategies
   ('explicit-with-timeout' and 'implicit') now publish to an inbox-
   aware set built from the event's full p-tag set (parent author
   + thread participants + @-mentions).

Behavior is purely additive — events with no p tags fall through to
existing relay strategy unchanged. Cache misses / lookup timeouts
fall back to base relays only, never block the publish.

Closes R9 from docs/perf/2026-04-review.md (#341).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(relay): address copilot review on PR #360

- Reformat src/lib/nip65Routing.ts with prettier (project's
  useTabs=false). The new file landed with tab indentation on first
  pass; this just runs prettier so it matches the rest of the repo.
- Normalize relay URLs before dedupe in unionInboxRelayUrls. Without
  normalization, 'wss://example.com' and 'wss://example.com/' would
  consume two slots in the 16-relay cap and could drop distinct
  relays. Reuses normalizeRelayUrl already exported from
  relayListCache. Original spelling is preserved in the result so
  NDK's pool keys (raw URLs) keep their existing connection state.
- Replace hardcoded garden / pantry literals in
  publishQueue.getBaseRelayUrls() with shared constants. Adds
  PANTRY_RELAY_URL to $lib/nostr next to the existing
  GARDEN_RELAY_URL so both have a single source of truth and changes
  propagate everywhere instead of drifting between literals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: spe1020 <sethsager@Seths-MacBook-Air.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant