From 4c54cfa6ff772ce33c83af740f8e2633de8591e3 Mon Sep 17 00:00:00 2001 From: lingniu Date: Thu, 16 Jul 2026 08:35:37 +0800 Subject: [PATCH] perf(web): pause hidden live polling --- .../src/v2/hooks/useMonitorData.cache.test.tsx | 4 ++++ .../apps/web/src/v2/hooks/useMonitorData.ts | 17 +++++++++++------ .../apps/web/src/v2/pages/HistoryPage.tsx | 4 ++-- .../apps/web/src/v2/pages/MonitorPage.tsx | 5 +++-- .../web/src/v2/pages/OperationsPage.test.tsx | 6 ++++++ .../apps/web/src/v2/pages/OperationsPage.tsx | 5 +++-- .../apps/web/src/v2/pages/VehiclePage.tsx | 4 ++-- .../apps/web/src/v2/queryPolicy.test.ts | 8 +++++++- .../apps/web/src/v2/queryPolicy.ts | 8 ++++++++ 9 files changed, 46 insertions(+), 15 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/hooks/useMonitorData.cache.test.tsx b/vehicle-data-platform/apps/web/src/v2/hooks/useMonitorData.cache.test.tsx index fd684714..837015ac 100644 --- a/vehicle-data-platform/apps/web/src/v2/hooks/useMonitorData.cache.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/hooks/useMonitorData.cache.test.tsx @@ -28,6 +28,10 @@ test('rapid viewport changes retain one workspace payload and avoid duplicate fl }); await waitFor(() => expect(monitorWorkspace).toHaveBeenCalledTimes(1)); + const workspaceQuery = client.getQueryCache().findAll({ queryKey: ['monitor', 'workspace'] })[0]; + const liveOptions = workspaceQuery?.options as { refetchIntervalInBackground?: boolean; refetchOnWindowFocus?: boolean }; + expect(liveOptions.refetchIntervalInBackground).toBe(false); + expect(liveOptions.refetchOnWindowFocus).toBe(true); expect(monitorSummary).not.toHaveBeenCalled(); expect(vehicleRealtime).not.toHaveBeenCalled(); for (let index = 1; index <= 8; index += 1) { diff --git a/vehicle-data-platform/apps/web/src/v2/hooks/useMonitorData.ts b/vehicle-data-platform/apps/web/src/v2/hooks/useMonitorData.ts index dfc2e163..070ca8f6 100644 --- a/vehicle-data-platform/apps/web/src/v2/hooks/useMonitorData.ts +++ b/vehicle-data-platform/apps/web/src/v2/hooks/useMonitorData.ts @@ -2,7 +2,7 @@ import { useQuery, type UseQueryResult } from '@tanstack/react-query'; import { useEffect, useRef, useState } from 'react'; import { api } from '../../api/client'; import type { MonitorMapResponse, MonitorSummary, MonitorWorkspaceResponse, Page, VehicleRealtimeRow } from '../../api/types'; -import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy'; +import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy'; export type MonitorFilters = { keyword: string; @@ -143,7 +143,8 @@ export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewpor placeholderData: retainPreviousPageWithinScope(filterScope, 2), staleTime: 5_000, gcTime: MONITOR_CACHE.mapGcTime, - refetchInterval: mapEnabled ? MONITOR_REFRESH.fleet : false + refetchInterval: mapEnabled ? MONITOR_REFRESH.fleet : false, + ...LIVE_QUERY_POLICY }); const summaryQuery = useQuery({ queryKey: ['monitor', 'summary', params.toString()], @@ -151,7 +152,8 @@ export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewpor enabled: !mapEnabled, placeholderData: workspace.data?.summary, refetchInterval: !mapEnabled ? MONITOR_REFRESH.summary : false, - gcTime: QUERY_MEMORY.summaryGcTime + gcTime: QUERY_MEMORY.summaryGcTime, + ...LIVE_QUERY_POLICY }); const vehiclesQuery = useQuery({ queryKey: ['monitor', 'vehicles', params.toString()], @@ -159,7 +161,8 @@ export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewpor enabled: vehicleEnabled && !mapEnabled, placeholderData: workspace.data?.vehicles, refetchInterval: vehicleEnabled && !mapEnabled ? MONITOR_REFRESH.fleet : false, - gcTime: QUERY_MEMORY.highVolumeGcTime + gcTime: QUERY_MEMORY.highVolumeGcTime, + ...LIVE_QUERY_POLICY }); const summary = mapEnabled ? projectWorkspaceQuery(workspace, (data) => data.summary) : summaryQuery; const vehicles = mapEnabled ? projectWorkspaceQuery>(workspace, (data) => data.vehicles) : vehiclesQuery; @@ -171,7 +174,8 @@ export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewpor enabled: Boolean(selectedVin), staleTime: 5_000, refetchInterval: selectedVin ? MONITOR_REFRESH.selected : false, - gcTime: QUERY_MEMORY.highVolumeGcTime + gcTime: QUERY_MEMORY.highVolumeGcTime, + ...LIVE_QUERY_POLICY }); return { summary, vehicles, map, selectedVehicle }; @@ -196,7 +200,8 @@ export function useMonitorVehicleCard(vin: string, vehicle?: VehicleRealtimeRow, enabled, staleTime: 10_000, refetchInterval: enabled && activelyTracked ? MONITOR_REFRESH.alerts : false, - gcTime: QUERY_MEMORY.highVolumeGcTime + gcTime: QUERY_MEMORY.highVolumeGcTime, + ...LIVE_QUERY_POLICY }); const address = useQuery({ queryKey: ['monitor', 'vehicle-card', 'address', addressPoint?.vin, addressPoint?.key], diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx index 1dbfd8d1..36757268 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx @@ -6,7 +6,7 @@ import { api } from '../../api/client'; import type { HistoryDataResponse, HistoryDataRow, HistoryExportRequest, HistoryMetricDefinition, HistorySeriesResponse } from '../../api/types'; import { buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, formatSeriesGrain, historyExportPollInterval, parseHistoryKeywords } from '../domain/history'; import { InlineError } from '../shared/AsyncState'; -import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy'; +import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy'; import { usePlatformSession } from '../auth/AuthGate'; import { canOperate } from '../auth/session'; @@ -50,7 +50,7 @@ function ExportJobsPanel() { queryKey: ['history-exports'], queryFn: ({ signal }) => api.historyExports(signal), refetchInterval: (current) => historyExportPollInterval(current.state.data), - refetchIntervalInBackground: false, + ...LIVE_QUERY_POLICY, gcTime: QUERY_MEMORY.highVolumeGcTime }); const jobs = query.data ?? []; diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx index 20b0066a..b008886c 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx @@ -8,7 +8,7 @@ import { FleetMap } from '../map/FleetMap'; import { EmptyState, InlineError } from '../shared/AsyncState'; import { formatNumber, relativeFreshness, statusLabel, vehicleStatus } from '../domain/monitor'; import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorFilterScope, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, useMonitorVehicleCard, type MonitorViewport } from '../hooks/useMonitorData'; -import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy'; +import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy'; const protocols = ['', 'GB32960', 'JT808', 'YUTONG_MQTT']; const statuses = ['', 'online', 'offline', 'driving', 'idle']; @@ -251,7 +251,8 @@ export default function MonitorPage() { queryKey: ['monitor', 'vehicle-list', listScope, listLimit, listOffset], queryFn: ({ signal }) => api.vehicleRealtime(listParams, signal), enabled: mode === 'list', placeholderData: retainPreviousPageWithinScope>(listScope, 2), refetchInterval: mode === 'list' ? MONITOR_REFRESH.fleet : false, - gcTime: QUERY_MEMORY.highVolumeGcTime + gcTime: QUERY_MEMORY.highVolumeGcTime, + ...LIVE_QUERY_POLICY }); const rows = useMemo(() => { const data = vehicles.data?.items ?? []; diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx index fd2f37c9..abddc98c 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx @@ -22,6 +22,12 @@ test('reconciles service identities with bound and identity-required vehicles', render(); expect(await screen.findByText('1035 / 234')).toBeInTheDocument(); + for (const key of [['ops-health-v2'], ['ops-source-readiness-v2']]) { + const query = client.getQueryCache().find({ queryKey: key }); + const liveOptions = query?.options as { refetchIntervalInBackground?: boolean; refetchOnWindowFocus?: boolean }; + expect(liveOptions.refetchIntervalInBackground).toBe(false); + expect(liveOptions.refetchOnWindowFocus).toBe(true); + } expect(screen.getByText('服务身份 / 在线')).toBeInTheDocument(); expect(screen.getByText('已绑定 1024 · 待绑定 11')).toBeInTheDocument(); expect(screen.queryByText('统一车辆视角')).not.toBeInTheDocument(); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx index df42d5ff..a4dbff88 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx @@ -2,14 +2,15 @@ import { IconRefresh } from '@douyinfe/semi-icons'; import { useQuery } from '@tanstack/react-query'; import { api } from '../../api/client'; import { InlineError } from '../shared/AsyncState'; +import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy'; function statusLabel(status: string) { return { ok: '正常', warning: '关注', error: '异常' }[status] ?? status; } export default function OperationsPage() { - const health = useQuery({ queryKey: ['ops-health-v2'], queryFn: ({ signal }) => api.opsHealth(signal), refetchInterval: 15_000, staleTime: 8_000 }); - const readiness = useQuery({ queryKey: ['ops-source-readiness-v2'], queryFn: ({ signal }) => api.sourceReadiness(signal), refetchInterval: 30_000, staleTime: 15_000 }); + const health = useQuery({ queryKey: ['ops-health-v2'], queryFn: ({ signal }) => api.opsHealth(signal), refetchInterval: 15_000, staleTime: 8_000, gcTime: QUERY_MEMORY.summaryGcTime, ...LIVE_QUERY_POLICY }); + const readiness = useQuery({ queryKey: ['ops-source-readiness-v2'], queryFn: ({ signal }) => api.sourceReadiness(signal), refetchInterval: 30_000, staleTime: 15_000, gcTime: QUERY_MEMORY.summaryGcTime, ...LIVE_QUERY_POLICY }); const data = health.data; const sources = readiness.data; const refresh = () => Promise.all([health.refetch(), readiness.refetch()]); return
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx index 5ecaca12..14de0b0b 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx @@ -9,7 +9,7 @@ import { api } from '../../api/client'; import type { LatestTelemetryResponse, QualityIssueRow, VehicleDetail, VehicleProfileSyncItem, VehicleProfileSyncResult } from '../../api/types'; import { usePlatformSession } from '../auth/AuthGate'; import { canAdminister } from '../auth/session'; -import { QUERY_MEMORY } from '../queryPolicy'; +import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy'; import { formatTelemetryTime, formatTelemetryValue, telemetryQualityLabel } from '../domain/telemetry'; import { formatZhNumber } from '../domain/formatters'; import { parseVehicleProfileSyncCSV, vehicleProfileSyncCSVHeader } from '../domain/profileSync'; @@ -198,7 +198,7 @@ function VehicleRecord({ detail, telemetry, telemetryPending, telemetryError, on export default function VehiclePage() { const { vin } = useParams(); const query = useQuery({ queryKey: ['vehicle-detail', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.vehicleDetail(new URLSearchParams({ keyword: vin!, limit: '20' }), signal), gcTime: QUERY_MEMORY.summaryGcTime }); - const telemetry = useQuery({ queryKey: ['vehicle-latest-telemetry', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.latestTelemetry(vin!, signal), staleTime: 10_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchInterval: 20_000, refetchIntervalInBackground: false }); + const telemetry = useQuery({ queryKey: ['vehicle-latest-telemetry', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.latestTelemetry(vin!, signal), staleTime: 10_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchInterval: 20_000, ...LIVE_QUERY_POLICY }); if (!vin) return ; if (query.isPending) return ; if (query.isError) return
query.refetch()} />
; diff --git a/vehicle-data-platform/apps/web/src/v2/queryPolicy.test.ts b/vehicle-data-platform/apps/web/src/v2/queryPolicy.test.ts index c76cf46b..86160cb7 100644 --- a/vehicle-data-platform/apps/web/src/v2/queryPolicy.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/queryPolicy.test.ts @@ -1,7 +1,13 @@ import { describe, expect, it } from 'vitest'; -import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from './queryPolicy'; +import { LIVE_QUERY_POLICY, QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from './queryPolicy'; describe('query memory policy', () => { + it('pauses live polling in hidden tabs and refreshes immediately on return', () => { + expect(LIVE_QUERY_POLICY).toEqual({ + refetchIntervalInBackground: false, + refetchOnWindowFocus: true + }); + }); it('drops high-volume route results immediately and bounds reusable small results', () => { expect(QUERY_MEMORY.highVolumeGcTime).toBe(0); expect(QUERY_MEMORY.optionGcTime).toBeLessThanOrEqual(30_000); diff --git a/vehicle-data-platform/apps/web/src/v2/queryPolicy.ts b/vehicle-data-platform/apps/web/src/v2/queryPolicy.ts index 16308370..e9491cd4 100644 --- a/vehicle-data-platform/apps/web/src/v2/queryPolicy.ts +++ b/vehicle-data-platform/apps/web/src/v2/queryPolicy.ts @@ -4,6 +4,14 @@ export const QUERY_MEMORY = { summaryGcTime: 60_000 } as const; +// Live views must not spend traffic while the browser tab is hidden, but they +// should reconcile immediately when an operator returns instead of waiting for +// the next interval tick. +export const LIVE_QUERY_POLICY = { + refetchIntervalInBackground: false, + refetchOnWindowFocus: true +} as const; + export function retainPreviousPageWithinScope(scope: string, scopeIndex = 1) { return (previous: T | undefined, previousQuery?: { queryKey: readonly unknown[] }) => previousQuery?.queryKey[scopeIndex] === scope ? previous : undefined; }