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:
@@ -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 };
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user