perf(ui): a next-page fetch joins the one in flight instead of sending the cursor again
react-query's fetchNextPage cancels a next page still loading and sends it again by default.
Our requests ignore the abort signal, so the server ran both: the feed's read-ahead effect
re-runs on scroll frames before isFetchingNextPage reaches it, and on prod every feed page
went out 2-3 times (a 0.8 s title sort queued to 2.3 s). Every call now passes NEXT_PAGE
({cancelRefetch: false}), lint-enforced, with a contract test.
This commit is contained in:
@@ -13,6 +13,7 @@ import { api, type LogFacet, type LogQuery, type ServerLogRow } from "../lib/api
|
||||
import { copyText } from "../lib/clipboard";
|
||||
import { formatDateTimeSec } from "../lib/format";
|
||||
import { nextNavId, notifyNav, rememberNavPosition } from "../lib/history";
|
||||
import { NEXT_PAGE } from "../lib/nextPage";
|
||||
import { LEVEL_GROUPS, type LevelGroup } from "../lib/log";
|
||||
import {
|
||||
DEFAULT_LOGS_STATE,
|
||||
@@ -156,7 +157,7 @@ export default function AdminLogs() {
|
||||
? "countsFailed"
|
||||
: null;
|
||||
const retryFailed = () => {
|
||||
if (logs.isFetchNextPageError) void logs.fetchNextPage();
|
||||
if (logs.isFetchNextPageError) void logs.fetchNextPage(NEXT_PAGE);
|
||||
else if (logs.isError) void logs.refetch();
|
||||
if (facets.isError) void facets.refetch();
|
||||
};
|
||||
@@ -190,7 +191,7 @@ export default function AdminLogs() {
|
||||
|
||||
const loadOlder = () => {
|
||||
setLive(false);
|
||||
void logs.fetchNextPage();
|
||||
void logs.fetchNextPage(NEXT_PAGE);
|
||||
};
|
||||
|
||||
const copy = async () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { NEXT_PAGE } from "../lib/nextPage";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Columns2,
|
||||
@@ -556,7 +557,7 @@ export default function Feed({
|
||||
<div className="text-center pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => ytQuery.fetchNextPage()}
|
||||
onClick={() => void ytQuery.fetchNextPage(NEXT_PAGE)}
|
||||
loading={ytQuery.isFetchingNextPage}
|
||||
>
|
||||
{ytQuery.isFetchingNextPage
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { User, X } from "lucide-react";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { NEXT_PAGE } from "../lib/nextPage";
|
||||
import { healPlexField } from "../lib/plexSearch";
|
||||
import { healPlexSort, plexSortOptions, PLEX_SORT_FALLBACK } from "../lib/plexSort";
|
||||
import type { SortState } from "../lib/columnSort";
|
||||
@@ -321,7 +322,8 @@ export default function PlexBrowse() {
|
||||
if (!sentinelEl) return;
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting && hasNextPage && !isFetchingNextPage) fetchNextPage();
|
||||
if (entries[0]?.isIntersecting && hasNextPage && !isFetchingNextPage)
|
||||
void fetchNextPage(NEXT_PAGE);
|
||||
},
|
||||
{ rootMargin: "600px" },
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Video, VideoStatus } from "../lib/api";
|
||||
import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
|
||||
import type { NEXT_PAGE } from "../lib/nextPage";
|
||||
import VideoCard from "./VideoCard";
|
||||
import { VideoMenu } from "./VideoMenu";
|
||||
import { useContextMenu } from "./ui/useContextMenu";
|
||||
@@ -29,7 +30,7 @@ export interface VirtualFeedProps {
|
||||
onOpen: (v: Video, startAt?: number | null) => void;
|
||||
hasNextPage: boolean;
|
||||
isFetchingNextPage: boolean;
|
||||
fetchNextPage: () => void;
|
||||
fetchNextPage: (options: typeof NEXT_PAGE) => unknown;
|
||||
}
|
||||
|
||||
// The feed's videos on the shared VirtualGrid engine — this wrapper only maps a FeedView to the
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useListNav } from "../../lib/listNav";
|
||||
import { scrollParent } from "../../lib/listPaging";
|
||||
import { NEXT_PAGE } from "../../lib/nextPage";
|
||||
import { GRID_GAP, columnsFor, itemWidthFor } from "./gridColumns";
|
||||
import { shouldPrefetchNext } from "./readAhead";
|
||||
|
||||
@@ -66,7 +67,8 @@ export interface VirtualGridProps<T> {
|
||||
/** Infinite scroll — omit all three for a finite client-side list. */
|
||||
hasNextPage?: boolean;
|
||||
isFetchingNextPage?: boolean;
|
||||
fetchNextPage?: () => void;
|
||||
/** Called with `NEXT_PAGE`, so a read-ahead that fires again joins the page already loading. */
|
||||
fetchNextPage?: (options: typeof NEXT_PAGE) => unknown;
|
||||
}
|
||||
|
||||
export default function VirtualGrid<T>({
|
||||
@@ -167,7 +169,7 @@ export default function VirtualGrid<T>({
|
||||
const last = virtualRows[virtualRows.length - 1];
|
||||
if (!last) return;
|
||||
if (shouldPrefetchNext(last.index, rows.length, virtualRows.length)) {
|
||||
fetchNextPage();
|
||||
fetchNextPage(NEXT_PAGE);
|
||||
}
|
||||
}, [virtualRows, rows.length, hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user