Skip to content

perf: decouple notification actions from the notifications query - #3426

Open
setchy wants to merge 2 commits into
mainfrom
perf/decouple-notification-action-subscriptions
Open

setchy wants to merge 2 commits into
mainfrom
perf/decouple-notification-action-subscriptions

Conversation

@setchy

@setchy setchy commented Oct 16, 2026

Copy link
Copy Markdown
Member

Summary

Alternative to #3320 for the same freeze: marking a notification stalls the renderer for ~8.5s on a 1,274-notification inbox.

The dominant cost is the deep structural-sharing walk TanStack Query runs on every notification cache write, and it is multiplied once per mounted observer. Every NotificationRow (and other action-only leaf components) called useNotifications(), so each mounted row subscribed to the notifications query as its own observer; a write then re-ran select and the structural-sharing walk once per row.

#3320 removes the walk by setting structuralSharing: false. That also removes the per-observer select-result dedup, so an unchanged poll stops being a no-op: every poll re-runs select, rebuilds the list items and re-renders consumers. This PR removes the amplification at its source instead and leaves structural sharing on.

Changes

  • useNotificationActions() — exposes markNotificationsAsRead, markNotificationsAsDone, unsubscribeNotification, removeAccountNotifications and refetchNotifications without calling useQuery.
  • useNotificationsQueryKey() — extracts the query-key derivation from the reader so the action surface targets the same cache entry without an observer.
  • Migrated to the action surface: NotificationRow, RepositoryHeader, primitives/Header, useLogins.
  • useNotifications() now composes the reader and the action surfaces; readers (route, sidebar, filters, accounts, shortcuts) are unchanged. removeAccountNotifications and refetchNotifications moved out of the reader hook.
  • Structural sharing stays at its default.
  • ARCHITECTURE.md documents the subscription rule.

Verification

  • pnpm test — 192 files / 1916 tests pass (28 new).
  • pnpm check and pnpm format:check clean.
  • New tests assert an action-only consumer adds no query observer and triggers no fetch, that 50 mounted rows add 0 observers, and that a mark-as-read through the action surface reaches the cache a reader observes.
  • Row-count independence is verified via observer count; a wall-clock profile on a real 1,000+ inbox has not been run.

Note

Merge one of this and #3320, not both. The matching spec change is gitify-app/gitify-specs#6 (now archived and synced into notification-state-composition).

Notification rows and other action-only leaf components called
`useNotifications()`, so each mounted row subscribed to the notifications
query as its own observer. Every cache write then re-ran the query's `select`
and its structural-sharing walk once per mounted row, which is what froze the
renderer for ~8.5s when marking a notification on a 1,274-notification inbox.

Add `useNotificationActions()`, which exposes the three mutations plus
`removeAccountNotifications` and `refetchNotifications` without calling
`useQuery`, and move the action-only consumers onto it: `NotificationRow`,
`RepositoryHeader`, `primitives/Header` and `useLogins`. The notifications
query key is extracted into `useNotificationsQueryKey()` so the action surface
targets the same cache entry as the readers without an observer.

Readers keep `useNotifications()`, which now composes the reader and action
surfaces, and structural sharing stays at its default. An unchanged poll
therefore remains a no-op for observers.

Alternative to #3320, which fixes the same freeze by disabling structural
sharing for the query and so also drops the poll-path reference stability.
@setchy
setchy requested a review from afonsojramos as a code owner October 16, 2026 12:17
@github-actions github-actions Bot added the refactor Refactoring of existing feature label Oct 16, 2026
The visual setup mirrors the app-hook mocks from vitest.setup.ts, but the new
useNotificationActions mock was only added to vitest.setup.ts. Visual tests
therefore rendered NotificationRow and RepositoryHeader against the real actions
hook, so `setNotificationsOverrides({ markNotificationsAsRead })` never applied:
the row stayed out of its pending/exit state and the real mutation attempted a
network request that the visual setup rejects.

Add the matching mock so action-only consumers resolve mocked actions in the
visual project too.
@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Refactoring of existing feature

Development

Successfully merging this pull request may close these issues.

1 participant