perf(web): bound speculative route warming

This commit is contained in:
lingniu
2026-07-16 03:39:47 +08:00
parent cad39d6326
commit 2f26fd25b9
5 changed files with 135 additions and 37 deletions

View File

@@ -3,6 +3,7 @@ import { lazy, type ComponentType } from 'react';
export type RouteSection = 'monitor' | 'vehicles' | 'tracks' | 'history' | 'statistics' | 'access' | 'alerts' | 'operations';
type RouteModule = { default: ComponentType };
type RoutePreloadConnection = { saveData?: boolean; effectiveType?: string };
type RoutePreloadRuntime = { visibilityState?: DocumentVisibilityState; deviceMemory?: number };
type RoutePreloadScheduler = (callback: () => void) => () => void;
const importers: Record<RouteSection, () => Promise<RouteModule>> = {
@@ -17,10 +18,16 @@ const importers: Record<RouteSection, () => Promise<RouteModule>> = {
};
const pendingModules = new Map<RouteSection, Promise<RouteModule>>();
const backgroundPreloadOrder: RouteSection[] = [
'monitor', 'vehicles', 'tracks', 'history', 'statistics', 'alerts', 'access', 'operations'
];
const backgroundPreloadBatchSize = 2;
const likelyNextRoutes: Record<RouteSection, RouteSection[]> = {
monitor: ['vehicles', 'tracks'],
vehicles: ['monitor', 'tracks'],
tracks: ['monitor', 'history'],
history: ['statistics', 'tracks'],
statistics: ['history', 'vehicles'],
access: ['monitor', 'operations'],
alerts: ['monitor', 'access'],
operations: ['access', 'monitor']
};
function loadRoute(section: RouteSection) {
const pending = pendingModules.get(section);
@@ -52,7 +59,35 @@ function browserConnection(): RoutePreloadConnection | undefined {
return (navigator as Navigator & { connection?: RoutePreloadConnection }).connection;
}
export function shouldPreloadRoutes(connection = browserConnection()) {
function browserRuntime(): RoutePreloadRuntime {
return {
visibilityState: document.visibilityState,
deviceMemory: (navigator as Navigator & { deviceMemory?: number }).deviceMemory
};
}
export function routePreloadBudget(connection = browserConnection(), runtime = browserRuntime()) {
if (runtime.visibilityState === 'hidden' || connection?.saveData) return 0;
const effectiveType = String(connection?.effectiveType ?? '').toLowerCase();
if (effectiveType.includes('2g') || (runtime.deviceMemory != null && runtime.deviceMemory <= 2)) return 0;
if (effectiveType.includes('3g') || (runtime.deviceMemory != null && runtime.deviceMemory <= 4)) return 1;
return 2;
}
export function shouldPreloadRoutes(connection = browserConnection(), runtime = browserRuntime()) {
return routePreloadBudget(connection, runtime) > 0;
}
export function routePreloadCandidates(pathname: string) {
const activeSection = routeSectionForPath(pathname);
return activeSection ? likelyNextRoutes[activeSection] : [];
}
function shouldContinuePreloading() {
return document.visibilityState !== 'hidden';
}
export function shouldPreloadRouteOnIntent(connection = browserConnection()) {
if (connection?.saveData) return false;
return !String(connection?.effectiveType ?? '').toLowerCase().includes('2g');
}
@@ -69,27 +104,31 @@ function scheduleWhenIdle(callback: () => void) {
export function scheduleIdleRoutePreloads({
activePathname,
connection = browserConnection(),
runtime = browserRuntime(),
preload = preloadRoute,
schedule = scheduleWhenIdle
schedule = scheduleWhenIdle,
isVisible = shouldContinuePreloading
}: {
activePathname: string;
connection?: RoutePreloadConnection;
runtime?: RoutePreloadRuntime;
preload?: (pathname: string) => Promise<unknown> | undefined;
schedule?: RoutePreloadScheduler;
isVisible?: () => boolean;
}) {
if (!shouldPreloadRoutes(connection)) return () => undefined;
const activeSection = routeSectionForPath(activePathname);
const queue = backgroundPreloadOrder.filter((section) => section !== activeSection);
const budget = routePreloadBudget(connection, runtime);
if (!budget) return () => undefined;
const queue = routePreloadCandidates(activePathname).slice(0, budget);
let cancelled = false;
let cancelScheduled: () => void = () => undefined;
const warmNext = () => {
if (cancelled) return;
const batch = queue.splice(0, backgroundPreloadBatchSize);
if (!batch.length) return;
void Promise.allSettled(batch.map((section) => Promise.resolve().then(() => preload(`/${section}`))))
if (cancelled || !isVisible()) return;
const section = queue.shift();
if (!section) return;
void Promise.resolve().then(() => preload(`/${section}`))
.finally(() => {
if (!cancelled) cancelScheduled = schedule(warmNext);
if (!cancelled && queue.length && isVisible()) cancelScheduled = schedule(warmNext);
});
};