perf: paginate and memoize list-heavy admin, social, and dashboard surfaces - #1147
Merged
RUKAYAT-CODER merged 1 commit intoAug 22, 2026
Conversation
…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
Contributor
|
Thank you for contributing to the project. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
src/hooks/usePagination.ts, plain client-side array slicing — noreact-window/AutoSizer, sinceAutoSizermeasures real DOM layout and renders 0 rows under jsdom, which would have broken every existing test for these components):Table.tsx— Prev/Next footer, defaultpageSize=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.TableRow,NotificationItem,UserRowwrapped inReact.memo; newApprovalItemRowandMediaQueueItemextracted from inline JSX and memoized.Table'stoggleSelectRowno longer depends onselectedRowKeys(it read the latest value via a ref instead) — previously its identity changed on every selection change, which would have busted every row'sonSelectprop, not just the toggled row's.ApprovalQueue's per-item review note moved out of parent state (aRecord<string, string>) into local state inside each row, so typing in one row's textarea no longer re-renders every other row.DashboardPanelCard.tsxwrapped inReact.memo— the one un-memoized piece ofAdvancedDashboard; everything else there (sortedPanels, all handlers,SortablePanel) was already memoized.SocialProfile.tsx: lightReact.memowrap only — it has no real follower/activity list to paginate today (placeholder tab content).BulkActions.tsx: no changes — it's already fullyuseCallback'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 ofnotificationcenter.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.Profilerin new*.bench.test.tsxfiles (no browser-profiling dependency, runs under Vitest/jsdom in CI):Full write-up with methodology and numbers:
docs/LIST_RENDERING_PERFORMANCE_REFACTOR.md.Test plan
pnpm run type-check— cleanpnpm run lint— cleanpnpm run build— succeedsvitestruns for every touched file — all green, including the previously-failing "Approve It" assertions insrc/app/api/approvals/__tests__/approvals.test.tsx, which now passTable,ApprovalQueue,notificationcenter(new test file),FollowingSystem(new test file),MediaManagerMediaManager.test.tsx(aDragEvent/spy quirk and aMath.random()-timed progress simulation) were confirmed identical onmainbefore this change (viagit stash) and are documented as out-of-scope in the docs file.