perf(web): warm frequent routes during idle time
This commit is contained in:
@@ -1,11 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { routeSectionForPath } from './routeModules';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { routeSectionForPath, scheduleIdleRoutePreloads, shouldPreloadRoutes } from './routeModules';
|
||||
|
||||
describe('route module preloading', () => {
|
||||
async function flushPreloadQueue() {
|
||||
for (let index = 0; index < 6; index += 1) await Promise.resolve();
|
||||
}
|
||||
|
||||
it('maps nested and parameterized URLs to the correct lazy module', () => {
|
||||
expect(routeSectionForPath('/monitor')).toBe('monitor');
|
||||
expect(routeSectionForPath('/vehicles/LTEST001?tab=telemetry')).toBe('vehicles');
|
||||
expect(routeSectionForPath('/alerts/rules#editor')).toBe('alerts');
|
||||
expect(routeSectionForPath('/not-a-platform-route')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips background route preloads on save-data and 2G connections', () => {
|
||||
expect(shouldPreloadRoutes({ saveData: true, effectiveType: '4g' })).toBe(false);
|
||||
expect(shouldPreloadRoutes({ effectiveType: 'slow-2g' })).toBe(false);
|
||||
expect(shouldPreloadRoutes({ effectiveType: '2g' })).toBe(false);
|
||||
expect(shouldPreloadRoutes({ effectiveType: '3g' })).toBe(true);
|
||||
expect(shouldPreloadRoutes({ effectiveType: '4g' })).toBe(true);
|
||||
});
|
||||
|
||||
it('warms inactive routes one idle period at a time and stops after cleanup', async () => {
|
||||
const callbacks: Array<() => void> = [];
|
||||
const cancelled: number[] = [];
|
||||
const schedule = vi.fn((callback: () => void) => {
|
||||
const index = callbacks.push(callback) - 1;
|
||||
return () => { cancelled.push(index); };
|
||||
});
|
||||
const preload = vi.fn(async () => undefined);
|
||||
const cleanup = scheduleIdleRoutePreloads({
|
||||
activePathname: '/monitor',
|
||||
connection: { effectiveType: '4g' },
|
||||
preload,
|
||||
schedule
|
||||
});
|
||||
|
||||
expect(schedule).toHaveBeenCalledTimes(1);
|
||||
callbacks[0]();
|
||||
await flushPreloadQueue();
|
||||
expect(preload).toHaveBeenCalledWith('/tracks');
|
||||
expect(schedule).toHaveBeenCalledTimes(2);
|
||||
|
||||
callbacks[1]();
|
||||
await flushPreloadQueue();
|
||||
expect(preload).toHaveBeenLastCalledWith('/history');
|
||||
cleanup();
|
||||
expect(cancelled).toContain(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user