From de5083ef18337b2990c3f07dc0b8a7ad2e33feb8 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 1 Jun 2026 01:27:53 +0800 Subject: [PATCH] Defer secondary chart detail loads --- .../LiveTemperatureThresholdChart.tsx | 107 +++++++++++++++--- .../__tests__/refreshCadencePolicy.test.ts | 41 +++++++ 2 files changed, 133 insertions(+), 15 deletions(-) diff --git a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx index 22c4ae84..88cdcf12 100644 --- a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx +++ b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx @@ -58,6 +58,9 @@ const PEAK_GLOW_BADGE_CLASS = { const PROBABILITY_REFRESH_AFTER_PATCH_MS = 60_000; const FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS = 90_000; const DETAIL_LOAD_BATCH_DELAY_MS = 0; +const INITIAL_DETAIL_LOAD_SLOTS = 3; +const DEFERRED_DETAIL_LOAD_DELAY_MS = 1_200; +const DEFERRED_DETAIL_LOAD_STEP_MS = 450; const TemperatureChartCanvas = dynamic( () => @@ -115,12 +118,46 @@ function shouldFetchCityDetailForChart({ city, documentHidden, isChartVisible, + compact = false, + isActive = false, + isMaximized = false, + slotIndex = 0, + detailLoadReady = true, }: { city: string; documentHidden: boolean; isChartVisible: boolean; + compact?: boolean; + isActive?: boolean; + isMaximized?: boolean; + slotIndex?: number; + detailLoadReady?: boolean; }) { - return Boolean(city) && isChartVisible && !documentHidden; + if (!city || !isChartVisible || documentHidden) return false; + if (!compact || isActive || isMaximized) return true; + if (normalizeSlotIndex(slotIndex) < INITIAL_DETAIL_LOAD_SLOTS) return true; + return detailLoadReady; +} + +function normalizeSlotIndex(slotIndex: number | null | undefined) { + return Number.isFinite(slotIndex) && Number(slotIndex) >= 0 ? Math.floor(Number(slotIndex)) : 0; +} + +function getInitialDetailLoadDelayMs({ + compact, + isActive, + isMaximized, + slotIndex, +}: { + compact?: boolean; + isActive?: boolean; + isMaximized?: boolean; + slotIndex?: number; +}) { + if (!compact || isActive || isMaximized) return 0; + const normalizedIndex = normalizeSlotIndex(slotIndex); + if (normalizedIndex < INITIAL_DETAIL_LOAD_SLOTS) return 0; + return DEFERRED_DETAIL_LOAD_DELAY_MS + (normalizedIndex - INITIAL_DETAIL_LOAD_SLOTS) * DEFERRED_DETAIL_LOAD_STEP_MS; } // ── Main component ───────────────────────────────────────────────────── @@ -136,6 +173,7 @@ export function LiveTemperatureThresholdChart({ isMaximized = false, disableClose = false, isActive = !compact, + slotIndex = 0, }: { isEn: boolean; row: ScanOpportunityRow | null; @@ -179,6 +217,11 @@ export function LiveTemperatureThresholdChart({ const [targetResolution, setTargetResolution] = useState(() => prefersHighFrequencyRunwayResolution(row, null) ? "1m" : "10m", ); + const detailLoadDelayMs = useMemo( + () => getInitialDetailLoadDelayMs({ compact, isActive, isMaximized, slotIndex }), + [compact, isActive, isMaximized, slotIndex], + ); + const [detailLoadReady, setDetailLoadReady] = useState(() => detailLoadDelayMs === 0); const [currentCityLocalDate, setCurrentCityLocalDate] = useState(() => formatCityLocalDate(row?.tz_offset_seconds), ); @@ -191,7 +234,7 @@ export function LiveTemperatureThresholdChart({ setTargetResolution(prefersHighFrequencyRunwayResolution(row, null) ? "1m" : "10m"); setHourly(seedHourlyForecastFromRow(row)); setLiveTemp(null); - setIsHourlyLoading(Boolean(city)); + setIsHourlyLoading(Boolean(city) && detailLoadDelayMs === 0); setDetailError(null); setDetailRetryNonce(0); setShowingStaleDetail(false); @@ -202,7 +245,22 @@ export function LiveTemperatureThresholdChart({ lastForegroundRefreshAtRef.current = 0; localDayRolloverFetchDateRef.current = ""; setCurrentCityLocalDate(formatCityLocalDate(row?.tz_offset_seconds)); - }, [city]); + }, [city, detailLoadDelayMs]); + + useEffect(() => { + if (!city) { + setDetailLoadReady(false); + return; + } + if (detailLoadDelayMs <= 0) { + setDetailLoadReady(true); + return; + } + + setDetailLoadReady(false); + const id = setTimeout(() => setDetailLoadReady(true), detailLoadDelayMs); + return () => clearTimeout(id); + }, [city, detailLoadDelayMs]); useEffect(() => { const node = chartVisibilityRef.current; @@ -235,17 +293,6 @@ export function LiveTemperatureThresholdChart({ return; } - if ( - !shouldFetchCityDetailForChart({ - city, - documentHidden: - typeof document !== "undefined" && document.visibilityState === "hidden", - isChartVisible, - }) - ) { - return; - } - const cacheKey = `${city}:${targetResolution}`; let cached = _hourlyCache.get(cacheKey); if (!cached || Date.now() - Number(cached.ts || 0) >= HOURLY_CACHE_TTL_MS) { @@ -270,6 +317,23 @@ export function LiveTemperatureThresholdChart({ return; } + if ( + !shouldFetchCityDetailForChart({ + city, + documentHidden: + typeof document !== "undefined" && document.visibilityState === "hidden", + isChartVisible, + compact, + isActive, + isMaximized, + slotIndex, + detailLoadReady, + }) + ) { + setIsHourlyLoading(false); + return; + } + if (!cached && !hasLoadedHourlyDetailRef.current) { setHourly(seedHourlyForecastFromRow(row)); setShowingStaleDetail(false); @@ -302,7 +366,19 @@ export function LiveTemperatureThresholdChart({ cancelled = true; clearTimeout(timer); }; - }, [city, row, targetResolution, isChartVisible, detailRetryNonce, isEn]); + }, [ + city, + row, + targetResolution, + isChartVisible, + compact, + isActive, + isMaximized, + slotIndex, + detailLoadReady, + detailRetryNonce, + isEn, + ]); useEffect(() => { if (!latestPatch || latestPatch.revision <= lastAppliedPatchRevisionRef.current) return; @@ -897,6 +973,7 @@ export const __getLiveObservationLabelsForTest = getLiveObservationLabels; export const __getObservationDisplayMetricsForTest = getObservationDisplayMetrics; export const __getPeakGlowStateForTest = getPeakGlowState; export const __getWundergroundDailyHighForTest = getWundergroundDailyHigh; +export const __getInitialDetailLoadDelayMsForTest = getInitialDetailLoadDelayMs; export const __shouldFetchCityDetailForChartForTest = shouldFetchCityDetailForChart; export const __shouldPollLiveChartForTest = shouldPollLiveChart; export const __mergePatchIntoHourlyForTest = mergePatchIntoHourly; diff --git a/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts index 9932b70e..5ee16fbd 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts @@ -6,6 +6,7 @@ import { } from "@/lib/refresh-policy"; import { scanTerminalQueryPolicy } from "@/components/dashboard/scan-terminal/scan-terminal-client"; import { + __getInitialDetailLoadDelayMsForTest, __shouldFetchCityDetailForChartForTest, __shouldPollLiveChartForTest, } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart"; @@ -164,6 +165,46 @@ export async function runTests() { __shouldFetchCityDetailForChartForTest({ city: "paris", documentHidden: true, isChartVisible: true }) === false, "hidden browser tabs should not prefetch city detail", ); + assert( + __getInitialDetailLoadDelayMsForTest({ compact: true, isActive: false, isMaximized: false, slotIndex: 0 }) === 0 && + __getInitialDetailLoadDelayMsForTest({ compact: true, isActive: false, isMaximized: false, slotIndex: 2 }) === 0, + "first visible grid charts should fetch full detail immediately", + ); + assert( + __getInitialDetailLoadDelayMsForTest({ compact: true, isActive: false, isMaximized: false, slotIndex: 3 }) > 0, + "later non-active grid charts should defer full-detail loading after first paint", + ); + assert( + __getInitialDetailLoadDelayMsForTest({ compact: true, isActive: true, isMaximized: false, slotIndex: 8 }) === 0 && + __getInitialDetailLoadDelayMsForTest({ compact: true, isActive: false, isMaximized: true, slotIndex: 8 }) === 0, + "active or maximized charts should bypass deferred detail loading", + ); + assert( + __shouldFetchCityDetailForChartForTest({ + city: "paris", + documentHidden: false, + isChartVisible: true, + compact: true, + isActive: false, + isMaximized: false, + slotIndex: 5, + detailLoadReady: false, + }) === false, + "deferred grid charts should not enter the detail request queue before their delay is ready", + ); + assert( + __shouldFetchCityDetailForChartForTest({ + city: "paris", + documentHidden: false, + isChartVisible: true, + compact: true, + isActive: false, + isMaximized: false, + slotIndex: 5, + detailLoadReady: true, + }) === true, + "deferred grid charts should fetch detail after their delay is ready", + ); const normalizedBatchDetail = __resolveCityDetailFromBatchForTest( { "hong kong": {