perf(web): bound speculative route warming
This commit is contained in:
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user