perf(ui): smaller initial page, slower idle polling

Photos grid was fetching per_page=500 on the very first request, which
serialized hundreds of thumbnail requests behind a single sort+payload.
Split into PER_PAGE_INITIAL=100 (one viewport, fast paint) and
PER_PAGE_BACKGROUND=500 (subsequent prefetch pages, fewer round-trips).

Idle polling for scan-status and worker-status was set to 10s / 15s
respectively. With nothing queued the typical session was firing 4–6
status requests every minute through the single uvicorn event loop on
top of everything else. Bumped both to 30s. While actively scanning /
processing the 2s / 3s cadence is unchanged — that's where the user
actually wants live updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claudio
2026-05-10 21:28:16 +02:00
parent 4bb2c959a8
commit b1c3ee68dd
3 changed files with 16 additions and 14 deletions

View File

@@ -28,20 +28,20 @@ export function ScanProgress() {
queryKey: ['scan-status'],
queryFn: () => library.scanStatus(),
refetchInterval: (query) =>
query.state.data?.is_scanning ? 2000 : 10000,
query.state.data?.is_scanning ? 2000 : 30000,
enabled: true,
})
const isScanning = scanStatus?.is_scanning ?? false
// Poll worker status to track vision queue activity.
// Fast polling (3s) while processing, slow (15s) otherwise.
// Fast polling (3s) while processing, slow (30s) otherwise.
const { data: workerStatus } = useQuery<WorkerStatus>({
queryKey: ['worker-status-progress'],
queryFn: () => library.maintenance.workerStatus(),
refetchInterval: (query) => {
const q = totalQueued(query.state.data)
return q > 0 ? 3000 : 15000
return q > 0 ? 3000 : 30000
},
enabled: true,
})

View File

@@ -81,17 +81,19 @@ export function usePhotosQuery() {
// seeks directly to the next slice via an indexed range scan,
// O(1) regardless of depth (no OFFSET skipping).
//
// MAX_PAGES is intentionally modest: 20 × 500 = 10 000 photos
// covers almost every browsing session up-front without burning
// through a 100k library on cold load. If a user scrolls past
// that horizon we'll add an infinite-query trigger; for now the
// cap keeps cold-load memory / network pressure sane.
const PER_PAGE = 500
// First page is small (~one viewport) so the grid paints fast;
// background pages are larger so we still cover ~10k photos in
// a few round-trips without burning through a 100k library on
// cold load. If a user scrolls past that horizon we'll add an
// infinite-query trigger; for now the cap keeps cold-load
// memory / network pressure sane.
const PER_PAGE_INITIAL = 100
const PER_PAGE_BACKGROUND = 500
const MAX_PAGES = 20
const INTER_PAGE_DELAY_MS = 50
const first = await fetchCursorPage(
{ per_page: PER_PAGE, ...filterParams },
{ per_page: PER_PAGE_INITIAL, ...filterParams },
signal,
)
const firstBatch = first.photos || []
@@ -104,7 +106,7 @@ export function usePhotosQuery() {
if (signal?.aborted) return
try {
const page = await fetchCursorPage(
{ per_page: PER_PAGE, cursor: nextCursor, ...filterParams },
{ per_page: PER_PAGE_BACKGROUND, cursor: nextCursor, ...filterParams },
signal,
)
if (signal?.aborted) return
@@ -114,7 +116,7 @@ export function usePhotosQuery() {
['photos', filterParams],
(prev) => (prev ? [...prev, ...more] : more)
)
if (!nextCursor || more.length < PER_PAGE) return
if (!nextCursor || more.length < PER_PAGE_BACKGROUND) return
// Yield a beat between pages so the main thread stays
// responsive (thumbnail decode, scroll handling) while
// we're back-filling in the background.

View File

@@ -49,14 +49,14 @@ export function useScanActivity() {
queryKey: ['scan-status'],
queryFn: () => library.scanStatus(),
refetchInterval: (query) =>
query.state.data?.is_scanning ? 2000 : 10000,
query.state.data?.is_scanning ? 2000 : 30000,
})
const { data: workerStatus } = useQuery<WorkerStatus>({
queryKey: ['worker-status-progress'],
queryFn: () => library.maintenance.workerStatus(),
refetchInterval: (query) => {
const data = query.state.data
return pendingTasks(data) + activeTasks(data) > 0 ? 3000 : 15000
return pendingTasks(data) + activeTasks(data) > 0 ? 3000 : 30000
},
})