diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index 6802759f..360487ed 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -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 }; diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index 0b22c319..4360b43f 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -862,10 +862,13 @@ export default function PlayerModal({ cancelled = true; window.clearInterval(timer); window.removeEventListener("pagehide", onHide); - // Final flush, then refresh the feed + playlists so resume bars reflect this session. + // Final flush, then mark the feed + playlists stale so resume bars reflect this session on the + // next natural refetch — WITHOUT refetching every loaded feed page on each player close (PERF-2: + // opening/closing the player through a watch session otherwise re-runs the whole infinite query + // each time). Progress is already persisted server-side; only the on-screen resume bar waits. Promise.resolve(persist()).finally(() => { - qc.invalidateQueries({ queryKey: qk.feed() }); - qc.invalidateQueries({ queryKey: qk.playlist() }); + qc.invalidateQueries({ queryKey: qk.feed(), refetchType: "none" }); + qc.invalidateQueries({ queryKey: qk.playlist(), refetchType: "none" }); }); const p = playerRef.current; if (p && typeof p.destroy === "function") {