7 changed files with 243 additions and 71 deletions
@ -0,0 +1,182 @@
@@ -0,0 +1,182 @@
|
||||
# Performance fixes (audit follow-up) |
||||
|
||||
This document records seven fixes identified during a sluggishness audit. They are ordered by impact and should be applied in sequence. |
||||
|
||||
## Executive summary |
||||
|
||||
Sluggishness came mainly from **main-thread work that scales with feed size** and **React context churn** that re-renders large subtrees. Orphaned files added maintenance noise but did not affect runtime. |
||||
|
||||
The largest win is in `NoteList`: expensive per-event work ran inside a filter that scans up to **2,500 events** on every dependency change. |
||||
|
||||
--- |
||||
|
||||
## Fix 1 — Hoist `pinnedEventHexIdSet` out of `shouldHideEvent` |
||||
|
||||
**File:** `src/components/NoteList/index.tsx` |
||||
|
||||
**Problem:** `shouldHideEvent` rebuilt `pinnedEventHexIdSet` (with `decode()` per pin) **inside** the per-event callback. That callback is used in a `useMemo` that scans up to `MAX_TIMELINE_EVENTS_SCAN_FOR_VISIBLE` (2500) rows, so pin decoding ran up to 2,500× per filter pass. |
||||
|
||||
**Fix:** Compute `pinnedEventHexIdSet` once in a `useMemo` keyed on `pinnedEventIds`. `shouldHideEvent` reads the memoized set. |
||||
|
||||
**Expected impact:** Largest feed-filter win; small diff. |
||||
|
||||
--- |
||||
|
||||
## Fix 2 — Cap profile-prefetch walks to visible + lookahead |
||||
|
||||
**File:** `src/components/NoteList/index.tsx` |
||||
|
||||
**Problem:** A debounced `useEffect` walked **all** of `timelineEventsForFilter` and `newEvents` on every buffer change, then enqueued profile/emoji prefetch for every author found. |
||||
|
||||
**Fix:** Walk only `filteredEvents.slice(0, showCount + prefetchMargin)` (capped, e.g. 120 rows) plus a small slice of `newEvents` (e.g. 32). Keep the existing note-stats slice for visible rows. |
||||
|
||||
**Expected impact:** Fewer redundant kind-0 fetches and less main-thread work when the timeline buffer grows. |
||||
|
||||
--- |
||||
|
||||
## Fix 3 — Memoize `MuteListProvider` and `ContentPolicyProvider` context values |
||||
|
||||
**Files:** |
||||
- `src/providers/MuteListProvider.tsx` |
||||
- `src/providers/ContentPolicyProvider.tsx` |
||||
|
||||
**Problem:** Both providers passed a new `value` object every render. Handler functions were recreated each render. Consumers include `NoteList`, `Note`, `Image`, `VideoPlayer`, etc., so parent re-renders cascaded through the feed. |
||||
|
||||
**Fix:** |
||||
- Wrap mute handlers in `useCallback`. |
||||
- Wrap policy updaters in `useCallback`. |
||||
- Wrap provider `value` in `useMemo` with stable dependencies. |
||||
|
||||
**Expected impact:** Fewer wide subtree re-renders when unrelated parent state changes. |
||||
|
||||
--- |
||||
|
||||
## Fix 4 — Cap `iterateProfileEvents` IndexedDB scan |
||||
|
||||
**File:** `src/services/indexed-db.service.ts` |
||||
|
||||
**Problem:** `iterateProfileEvents` loaded **all** kind-0 rows from IndexedDB into a single `events[]` array before chunked callback processing. Large offline caches caused a big allocation on first @-mention search or session prewarm. |
||||
|
||||
**Fix:** Cap rows collected during the cursor pass (`MAX_PROFILE_EVENTS_ITERATE`, e.g. 8,000). Log when truncated. Keep chunked `requestAnimationFrame` yields during callback processing. |
||||
|
||||
**Expected impact:** Bounded memory and faster startup on accounts with very large profile caches. |
||||
|
||||
--- |
||||
|
||||
## Fix 5 — Tune `ReplaceableEventService` batching during scroll |
||||
|
||||
**Files:** |
||||
- `src/lib/scroll-activity.service.ts` (new) |
||||
- `src/components/NoteList/index.tsx` |
||||
- `src/services/client-replaceable-events.service.ts` |
||||
|
||||
**Problem:** `maxBatchSize: 200` with 100ms batching queued many kind-0 fetches during fast scrolling, competing with timeline REQs (console: `Large batch detected, limiting processing`). |
||||
|
||||
**Fix:** |
||||
- Add a lightweight scroll-activity signal (`markScrolling()` from feed scroll handlers). |
||||
- Lower `maxBatchSize` (64) and lengthen `batchScheduleFn` delay while scrolling (200ms vs 100ms idle). |
||||
|
||||
**Expected impact:** Profile fetches defer during scroll; timeline paint stays responsive. |
||||
|
||||
--- |
||||
|
||||
## Fix 6 — Delete unused files and junk |
||||
|
||||
**Problem:** Knip reported 16 unused files plus `src/services/Untitled` (accidental single-character junk). No runtime cost, but bundle/clarity noise. |
||||
|
||||
**Files removed:** |
||||
|
||||
| Cluster | Files | |
||||
|---------|--------| |
||||
| Connected relays UI | `ActiveRelaysDropdownSection.tsx`, `ActiveRelaysIconGrid.tsx`, `active-relays-display.ts` | |
||||
| Old Explore tabs | `Explore/index.tsx`, `ExploreFavoriteRelays.tsx`, `ExplorePopularRelays.tsx`, `ExploreRelayReviews.tsx` | |
||||
| Other UI | `FollowingFavoriteRelayList`, `SeenOnButton`, `NotificationThreadWatchButtons`, `ProfileTimeline`, `Tabs/index.tsx`, `ProfileSearchBar`, `QuickZapSwitch` | |
||||
| Hooks | `useBtcUsdRate.ts`, `useRelayConnectionRows.ts` | |
||||
| Junk | `src/services/Untitled` | |
||||
|
||||
**Note:** `useProfileTimeline` hook and `ExploreRelayDirectory` remain in use; only dead wrappers were removed. |
||||
|
||||
--- |
||||
|
||||
## Fix 7 — YouTube iframe API poll timeout |
||||
|
||||
**File:** `src/lib/youtube-iframe-api.ts` |
||||
|
||||
**Problem:** When the YouTube script tag was already present but `YT.Player` never became available, `requestAnimationFrame` polled forever. |
||||
|
||||
**Fix:** Cap polling (e.g. 5s / ~300 frames). Resolve the promise anyway so embeds fail gracefully instead of leaking rAF work. |
||||
|
||||
--- |
||||
|
||||
## Verification |
||||
|
||||
After applying all fixes: |
||||
|
||||
```bash |
||||
npm run test -- --run src/lib/profile-author-warmup-spec.test.ts src/components/PostEditor/PostTextarea/Mention/mention-range.test.ts |
||||
npx tsc --noEmit |
||||
``` |
||||
|
||||
Manual checks: |
||||
|
||||
1. Home feed scroll — no jank spike when many authors are visible. |
||||
2. @-mention search — still finds cached profiles after IDB cap. |
||||
3. YouTube embeds — still load when API is healthy; no infinite rAF when blocked. |
||||
|
||||
--- |
||||
|
||||
## Phase 2 — Archives API was additive, not a replacement (2026-06-07) |
||||
|
||||
**Why it still felt sluggish:** Nostr Archives REST was wired in parallel with relays for profiles/search, but **feed note stats still fired full relay REQ waves** for every visible card even when Archives already had interaction counts. Archives also consumed the shared **100 req/min** budget via sequential interaction prefetch, while relay work continued unchanged. |
||||
|
||||
### Fix A — Feed cards: Archives-only stats |
||||
|
||||
**File:** `src/components/NoteStats/index.tsx` |
||||
|
||||
Background feed cards (`!foregroundStats`) now call `prefetchArchivesInteractions` only. Relay `fetchNoteStats` runs on foreground surfaces (open note, thread, interactions panel). |
||||
|
||||
### Fix B — Parallel Archives interaction prefetch |
||||
|
||||
**File:** `src/lib/note-stats-archives-prefetch.ts` |
||||
|
||||
Prefetch runs with concurrency 5 instead of one note at a time. |
||||
|
||||
### Fix C — Archives-first profile batch |
||||
|
||||
**File:** `src/lib/profile-metadata-batch.ts` |
||||
|
||||
Await Archives metadata (400ms cap), then relay-fetch **only pubkeys Archives did not return** — avoids duplicate kind-0 storms. |
||||
|
||||
### Fix D — Remove 120× `subscribeNoteStats` in NoteList |
||||
|
||||
**File:** `src/components/NoteList/index.tsx` |
||||
|
||||
Dropped per-visible-note stats subscriptions that re-triggered profile batches on every Archives count update. |
||||
|
||||
### Fix E — Stabilize superchat filter |
||||
|
||||
**File:** `src/components/NoteList/index.tsx` |
||||
|
||||
`feedAttestedSuperchatIds` read from a ref inside `shouldHideEvent` so attestation updates do not rescan 2500 rows. |
||||
|
||||
### Fix F — Cap Archives blocking on thread resolve |
||||
|
||||
**File:** `src/lib/thread-context-local.ts` |
||||
|
||||
Archives event lookup races with a 700ms cap before falling through to local stores / relays. |
||||
|
||||
--- |
||||
|
||||
## Related audit items (not in this pass) |
||||
|
||||
| Area | Notes | |
||||
|------|--------| |
||||
| `DeepBrowsingProvider` + `Tabs` | Scroll updates context → tab chrome re-renders | |
||||
| `NostrProvider` | Account hydration causes broad tree updates | |
||||
| `useFeedAttestedSuperchatIds` | New attestations invalidate `shouldHideEvent` → full feed re-scan | |
||||
| `NoteList` stats subscriptions | Up to ~120 `subscribeNoteStats` listeners | |
||||
| `RssFeedList` | 20s IndexedDB poll while mounted | |
||||
| `client.service` | 45s HTTP timeline polling on index relays | |
||||
| Knip unused exports | Mostly shadcn re-exports; low priority | |
||||
|
||||
The `while (true)` worker pool in `client.service.ts` (~line 390) is intentional and bounded. |
||||
Loading…
Reference in new issue