Defer secondary chart detail loads

This commit is contained in:
2569718930@qq.com
2026-06-01 01:27:53 +08:00
parent 2424b68532
commit de5083ef18
2 changed files with 133 additions and 15 deletions
@@ -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<string>(() =>
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;
@@ -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": {