perf(web): warm frequent routes during idle time

This commit is contained in:
lingniu
2026-07-16 01:19:29 +08:00
parent b93e165590
commit 8ebffa4fee
5 changed files with 137 additions and 9 deletions

View File

@@ -2,6 +2,8 @@ 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 RoutePreloadScheduler = (callback: () => void) => () => void;
const importers: Record<RouteSection, () => Promise<RouteModule>> = {
monitor: () => import('../pages/MonitorPage'),
@@ -15,6 +17,9 @@ const importers: Record<RouteSection, () => Promise<RouteModule>> = {
};
const pendingModules = new Map<RouteSection, Promise<RouteModule>>();
const backgroundPreloadOrder: RouteSection[] = [
'monitor', 'tracks', 'history', 'statistics'
];
function loadRoute(section: RouteSection) {
const pending = pendingModules.get(section);
@@ -30,9 +35,70 @@ export function routeSectionForPath(pathname: string): RouteSection | undefined
return section && section in importers ? section : undefined;
}
export function preloadRoute(pathname: string) {
export async function preloadRoute(pathname: string) {
const section = routeSectionForPath(pathname);
return section ? loadRoute(section) : undefined;
if (!section) return undefined;
try {
return await loadRoute(section);
} catch {
// Intent/background preloads are speculative. loadRoute already evicts the
// failed Promise so a later navigation can retry through the route boundary.
return undefined;
}
}
function browserConnection(): RoutePreloadConnection | undefined {
return (navigator as Navigator & { connection?: RoutePreloadConnection }).connection;
}
export function shouldPreloadRoutes(connection = browserConnection()) {
if (connection?.saveData) return false;
return !String(connection?.effectiveType ?? '').toLowerCase().includes('2g');
}
function scheduleWhenIdle(callback: () => void) {
if (typeof window.requestIdleCallback === 'function') {
const id = window.requestIdleCallback(callback, { timeout: 2_000 });
return () => window.cancelIdleCallback(id);
}
const id = window.setTimeout(callback, 900);
return () => window.clearTimeout(id);
}
export function scheduleIdleRoutePreloads({
activePathname,
connection = browserConnection(),
preload = preloadRoute,
schedule = scheduleWhenIdle
}: {
activePathname: string;
connection?: RoutePreloadConnection;
preload?: (pathname: string) => Promise<unknown> | undefined;
schedule?: RoutePreloadScheduler;
}) {
if (!shouldPreloadRoutes(connection)) return () => undefined;
const activeSection = routeSectionForPath(activePathname);
const queue = backgroundPreloadOrder.filter((section) => section !== activeSection);
let cancelled = false;
let cancelScheduled: () => void = () => undefined;
const warmNext = () => {
if (cancelled) return;
const section = queue.shift();
if (!section) return;
void Promise.resolve()
.then(() => preload(`/${section}`))
.catch(() => undefined)
.finally(() => {
if (!cancelled) cancelScheduled = schedule(warmNext);
});
};
cancelScheduled = schedule(warmNext);
return () => {
cancelled = true;
cancelScheduled();
};
}
export const RoutePages = {