perf(feed): stop the card-action + player-close feed refetch storm (E2 S5, PERF-2)

Marking a card watched/hidden/reset and closing the player each fired a full
`invalidateQueries(qk.feed())`, refetching EVERY loaded infinite-query page — 10
pages x a few quick marks = ~50 feed refetches, each a full scan (x PERF-1). The
optimistic `overrides` layer + the matchesView filter already render the correct
state (a hidden/watched card drops out instantly), so the refetch was pure waste.

Switch these paths to `refetchType: "none"`: the queries are marked stale but not
refetched now; the next filter change / remount pulls fresh server data. Applies
to the two Feed card-action paths (setState/clearState) and the PlayerModal
close (feed + playlists — progress is already persisted server-side, only the
on-screen resume bar waits for the next natural refetch).

Left as immediate refetch on purpose: channel/tag/playlist mutations (one-off, on
pages where the main feed is unmounted anyway) and the onboarding / notification-
undo paths (which must reveal the changed content right away). Gate green:
frontend tsc/eslint/knip/prettier/222 vitest (backend unchanged, 344 pytest).
This commit is contained in:
2026-08-18 15:49:49 +02:00
parent 21a3aae700
commit 943143ef45
2 changed files with 13 additions and 5 deletions
+7 -2
View File
@@ -254,7 +254,11 @@ export default function Feed({
api
.setState(id, status)
.then(() => {
qc.invalidateQueries({ queryKey: qk.feed() });
// Mark stale WITHOUT refetching: the optimistic `overrides` layer + the matchesView filter
// already show the correct state (a hidden/watched card drops out immediately), so refetching
// every loaded infinite-query page here was pure waste — 10 pages x rapid marks = a refetch
// storm (PERF-2). The next filter change / remount refetches fresh from the server.
qc.invalidateQueries({ queryKey: qk.feed(), refetchType: "none" });
if (status === "hidden") {
const v = loadedRef.current.find((x) => x.id === id);
notify({
@@ -311,7 +315,8 @@ export default function Feed({
setOverrides((o) => ({ ...o, [id]: "new" }));
api
.clearState(id)
.then(() => qc.invalidateQueries({ queryKey: qk.feed() }))
// Stale-mark only (see setState above): the optimistic override already reset the card.
.then(() => qc.invalidateQueries({ queryKey: qk.feed(), refetchType: "none" }))
.catch(() =>
setOverrides((o) => {
const next = { ...o };