Skip to content

perf: paginate and memoize list-heavy admin, social, and dashboard surfaces - #1147

Merged
RUKAYAT-CODER merged 1 commit into
rinafcode:mainfrom
TheWeirdDee:perf/list-rendering-pagination-memoization
Aug 22, 2026
Merged

perf: paginate and memoize list-heavy admin, social, and dashboard surfaces#1147
RUKAYAT-CODER merged 1 commit into
rinafcode:mainfrom
TheWeirdDee:perf/list-rendering-pagination-memoization

Conversation

@TheWeirdDee

Copy link
Copy Markdown
Contributor

Summary

Closes #1136.

Several high-traffic surfaces rendered their entire in-memory collection on every render and rebuilt per-row/per-item callbacks (and, in several cases, unmemoized row components) on every render, so render cost scaled with list size on every keystroke, selection toggle, or unrelated state change.

  • Pagination (new shared src/hooks/usePagination.ts, plain client-side array slicing — no react-window/AutoSizer, since AutoSizer measures real DOM layout and renders 0 rows under jsdom, which would have broken every existing test for these components):
    • Table.tsx — Prev/Next footer, default pageSize=25 (overridable).
    • ApprovalQueue.tsx — Prev/Next footer, page size 10.
    • FollowingSystem.tsx — Prev/Next footer, page size 15, resets to page 1 on tab switch or search change.
    • notificationcenter.tsx / MediaManager.tsx — "Load more" (accumulates rather than replaces), page size 20 / 10.
  • Row/item memoization: TableRow, NotificationItem, UserRow wrapped in React.memo; new ApprovalItemRow and MediaQueueItem extracted from inline JSX and memoized.
  • Callback stabilization:
    • Table's toggleSelectRow no longer depends on selectedRowKeys (it read the latest value via a ref instead) — previously its identity changed on every selection change, which would have busted every row's onSelect prop, not just the toggled row's.
    • ApprovalQueue's per-item review note moved out of parent state (a Record<string, string>) into local state inside each row, so typing in one row's textarea no longer re-renders every other row.
  • DashboardPanelCard.tsx wrapped in React.memo — the one un-memoized piece of AdvancedDashboard; everything else there (sortedPanels, all handlers, SortablePanel) was already memoized.
  • SocialProfile.tsx: light React.memo wrap only — it has no real follower/activity list to paginate today (placeholder tab content).
  • BulkActions.tsx: no changes — it's already fully useCallback'd and renders a small static action bar, not a data-driven list.

Drive-by fixes

Found while touching these files, fixed in the same diff:

  • notificationcenter.tsx: an invalid JSX expression in the no-avatar fallback ({TYPE_ICON[type]} double-wrapped in braces).
  • ApprovalQueue.tsx: the Approve button read "Approve It" but the existing test asserted an exact-match /^approve$/i — this was failing independent of this refactor. Renamed to "Approve" (consistent with the single-word "Reject" label).

Note: this branch also rebases onto the latest main (which had moved 4 commits ahead, including an a11y refactor of notificationcenter.tsx — focus trap, aria-expanded/aria-controls, Escape-to-close). That work is preserved; pagination/memoization is layered on top of it, not instead of it.

Before / after

Captured via React.Profiler in new *.bench.test.tsx files (no browser-profiling dependency, runs under Vitest/jsdom in CI):

[bench:Table] select 1/300 rows -> before: ~50ms (unmemoized) | after: ~0ms (memoized rows, stable onSelect)
[bench:NotificationCenter] update after 1/50 changed -> 1 commit(s), ~1ms total actualDuration
[bench:FollowingSystem] 1 row follow-toggle / 50 rows -> 1 commit(s), ~0.3ms total actualDuration
[bench:DashboardPanelCard] unchanged-props re-render -> 1 commit(s), ~0.03ms actualDuration (memoized bailout)

Full write-up with methodology and numbers: docs/LIST_RENDERING_PERFORMANCE_REFACTOR.md.

Test plan

  • pnpm run type-check — clean
  • pnpm run lint — clean
  • pnpm run build — succeeds
  • Targeted vitest runs for every touched file — all green, including the previously-failing "Approve It" assertions in src/app/api/approvals/__tests__/approvals.test.tsx, which now pass
  • New regression tests: pagination behavior, page-reset-on-filter, row-memoization/callback-isolation, for Table, ApprovalQueue, notificationcenter (new test file), FollowingSystem (new test file), MediaManager
  • New before/after benchmark tests (see above)
  • Two pre-existing, unrelated flaky tests in MediaManager.test.tsx (a DragEvent/spy quirk and a Math.random()-timed progress simulation) were confirmed identical on main before this change (via git stash) and are documented as out-of-scope in the docs file.

…rfaces

Table.tsx, notificationcenter.tsx, ApprovalQueue.tsx, MediaManager.tsx, and
FollowingSystem.tsx rendered their entire in-memory collection on every
render and rebuilt per-row/per-item callbacks (and, for Table/ApprovalQueue/
notificationcenter, unmemoized row components) on every render, so cost grew
with list size on every keystroke or unrelated state change.

- Add a shared src/hooks/usePagination.ts (client-side, array-slicing) and
  use it to paginate Table (Prev/Next), ApprovalQueue (Prev/Next),
  FollowingSystem (Prev/Next, resets on tab/search change), notificationcenter
  and MediaManager ("Load more", accumulates).
- Extract and React.memo row/item components: TableRow, ApprovalItemRow
  (new), NotificationItem, MediaQueueItem (new), UserRow.
- Stabilize the callbacks passed to those rows: Table's toggleSelectRow no
  longer depends on selectedRowKeys (was busting every row's onSelect on any
  selection change); ApprovalQueue's review note moved from parent state into
  local per-row state so typing doesn't re-render sibling rows.
- Wrap DashboardPanelCard in React.memo (the one un-memoized piece of
  AdvancedDashboard; everything else there was already memoized).
- SocialProfile.tsx: light React.memo wrap only — it has no real list to
  paginate today. BulkActions.tsx: no changes — already fully useCallback'd
  and renders a small static action bar, not a data-driven list.
- Fix two pre-existing bugs found while touching these files: a broken JSX
  expression in notificationcenter's no-avatar fallback, and ApprovalQueue's
  "Approve It" button text not matching the existing test's exact-match
  regex (both previously-failing/unguarded).
- Add regression tests (pagination, callback isolation, review-note
  isolation) and React.Profiler-based before/after benchmarks; add
  docs/LIST_RENDERING_PERFORMANCE_REFACTOR.md with before/after numbers.

Closes rinafcode#1136
@RUKAYAT-CODER

Copy link
Copy Markdown
Contributor

Thank you for contributing to the project.

@RUKAYAT-CODER
RUKAYAT-CODER merged commit 76c38f8 into rinafcode:main Aug 22, 2026
6 checks passed
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.

Paginate and memoize the list-heavy admin, social, and dashboard surfaces

2 participants