import { type ReactNode, useEffect, useRef, useState } from 'react'; import { collectRouteImageUrls, hasCssImageUrlChange, type ImagePreloadResult, } from './routeImageReadyGateUtils'; import { RouteLoadingScreen } from './RouteLoadingScreen'; type RouteImageReadyGateProps = { children: ReactNode; eyebrow: string; text: string; }; const IMAGE_GATE_QUIET_MS = 140; const IMAGE_GATE_MIN_VISIBLE_WAIT_MS = 260; const IMAGE_GATE_MAX_BLOCK_MS = 1400; const IMAGE_PRELOAD_TIMEOUT_MS = 12000; const settledImageUrls = new Set(); const imagePreloadTasks = new Map>(); function preloadImageUrl(url: string) { const existingTask = imagePreloadTasks.get(url); if (existingTask) { return existingTask; } const task = new Promise((resolve) => { if (typeof Image === 'undefined') { resolve({ url, status: 'loaded' }); return; } const image = new Image(); let settled = false; const settle = (status: ImagePreloadResult['status']) => { if (settled) { return; } settled = true; window.clearTimeout(timeoutId); settledImageUrls.add(url); resolve({ url, status }); }; const timeoutId = window.setTimeout(() => { settle('timeout'); }, IMAGE_PRELOAD_TIMEOUT_MS); image.onload = () => settle('loaded'); image.onerror = () => settle('failed'); image.decoding = 'async'; image.src = url; if (image.complete) { settle(image.naturalWidth > 0 ? 'loaded' : 'failed'); } }); imagePreloadTasks.set(url, task); return task; } /** * 路由首屏图片门闩:业务页面先真实挂载但不可见, * 只等待短暂稳定窗口,不再把所有图片加载完成作为首屏硬阻塞。 */ export function RouteImageReadyGate({ children, eyebrow, text, }: RouteImageReadyGateProps) { const rootRef = useRef(null); const startTimeRef = useRef(0); const scanTimerRef = useRef(null); const revealTimerRef = useRef(null); const scanVersionRef = useRef(0); const revealedRef = useRef(false); const [ready, setReady] = useState(false); useEffect(() => { const root = rootRef.current; if (!root) { return; } let disposed = false; startTimeRef.current = window.performance.now(); revealedRef.current = false; setReady(false); const clearScanTimer = () => { if (scanTimerRef.current !== null) { window.clearTimeout(scanTimerRef.current); scanTimerRef.current = null; } }; const clearRevealTimer = () => { if (revealTimerRef.current !== null) { window.clearTimeout(revealTimerRef.current); revealTimerRef.current = null; } }; const scheduleScan = () => { clearScanTimer(); scanTimerRef.current = window.setTimeout(runScan, IMAGE_GATE_QUIET_MS); }; const scheduleReveal = (version: number) => { if (revealedRef.current) { return; } clearRevealTimer(); const elapsed = window.performance.now() - startTimeRef.current; const preferredDelay = Math.max( IMAGE_GATE_QUIET_MS, IMAGE_GATE_MIN_VISIBLE_WAIT_MS - elapsed, ); const maxRemainingDelay = Math.max(0, IMAGE_GATE_MAX_BLOCK_MS - elapsed); const delay = Math.min(preferredDelay, maxRemainingDelay); revealTimerRef.current = window.setTimeout(() => { if (disposed || version !== scanVersionRef.current) { return; } revealedRef.current = true; setReady(true); }, delay); }; function runScan() { if (disposed) { return; } const version = scanVersionRef.current + 1; scanVersionRef.current = version; const pendingUrls = collectRouteImageUrls(root).filter( (url) => !settledImageUrls.has(url), ); if (pendingUrls.length > 0) { // 首屏慢加载的核心约束:图片可预热,但不能无限期阻塞页面主体可见。 pendingUrls.forEach((url) => { void preloadImageUrl(url); }); } scheduleReveal(version); } const observer = new MutationObserver((mutations) => { if (disposed || revealedRef.current) { return; } const shouldRescan = mutations.some((mutation) => { if (mutation.type === 'childList') { return ( mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0 ); } if (mutation.type !== 'attributes') { return false; } if ( mutation.attributeName === 'src' || mutation.attributeName === 'srcset' ) { return true; } return mutation.attributeName === 'style' ? hasCssImageUrlChange(mutation) : false; }); if (!shouldRescan) { return; } clearRevealTimer(); scheduleScan(); }); observer.observe(root, { attributes: true, attributeFilter: ['src', 'srcset', 'style'], attributeOldValue: true, childList: true, subtree: true, }); scheduleScan(); return () => { disposed = true; observer.disconnect(); clearScanTimer(); clearRevealTimer(); }; }, []); return ( <>
{children}
{!ready ? (
) : null} ); }