perf(web): warm every primary route
This commit is contained in:
@@ -21,14 +21,14 @@ describe('route module preloading', () => {
|
|||||||
expect(shouldPreloadRoutes({ effectiveType: '4g' })).toBe(true);
|
expect(shouldPreloadRoutes({ effectiveType: '4g' })).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('warms inactive routes one idle period at a time and stops after cleanup', async () => {
|
it('warms every inactive primary route in bounded idle batches and stops after cleanup', async () => {
|
||||||
const callbacks: Array<() => void> = [];
|
const callbacks: Array<() => void> = [];
|
||||||
const cancelled: number[] = [];
|
const cancelled: number[] = [];
|
||||||
const schedule = vi.fn((callback: () => void) => {
|
const schedule = vi.fn((callback: () => void) => {
|
||||||
const index = callbacks.push(callback) - 1;
|
const index = callbacks.push(callback) - 1;
|
||||||
return () => { cancelled.push(index); };
|
return () => { cancelled.push(index); };
|
||||||
});
|
});
|
||||||
const preload = vi.fn(async () => undefined);
|
const preload = vi.fn(async (_pathname: string) => undefined);
|
||||||
const cleanup = scheduleIdleRoutePreloads({
|
const cleanup = scheduleIdleRoutePreloads({
|
||||||
activePathname: '/monitor',
|
activePathname: '/monitor',
|
||||||
connection: { effectiveType: '4g' },
|
connection: { effectiveType: '4g' },
|
||||||
@@ -39,13 +39,22 @@ describe('route module preloading', () => {
|
|||||||
expect(schedule).toHaveBeenCalledTimes(1);
|
expect(schedule).toHaveBeenCalledTimes(1);
|
||||||
callbacks[0]();
|
callbacks[0]();
|
||||||
await flushPreloadQueue();
|
await flushPreloadQueue();
|
||||||
expect(preload).toHaveBeenCalledWith('/tracks');
|
expect(preload.mock.calls.map(([path]) => path)).toEqual(['/vehicles', '/tracks']);
|
||||||
expect(schedule).toHaveBeenCalledTimes(2);
|
expect(schedule).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
callbacks[1]();
|
callbacks[1]();
|
||||||
await flushPreloadQueue();
|
await flushPreloadQueue();
|
||||||
expect(preload).toHaveBeenLastCalledWith('/history');
|
expect(preload.mock.calls.map(([path]) => path)).toEqual(['/vehicles', '/tracks', '/history', '/statistics']);
|
||||||
|
|
||||||
|
callbacks[2]();
|
||||||
|
await flushPreloadQueue();
|
||||||
|
callbacks[3]();
|
||||||
|
await flushPreloadQueue();
|
||||||
|
expect(preload.mock.calls.map(([path]) => path)).toEqual([
|
||||||
|
'/vehicles', '/tracks', '/history', '/statistics', '/alerts', '/access', '/operations'
|
||||||
|
]);
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
expect(cancelled).toContain(2);
|
expect(cancelled).toContain(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ const importers: Record<RouteSection, () => Promise<RouteModule>> = {
|
|||||||
|
|
||||||
const pendingModules = new Map<RouteSection, Promise<RouteModule>>();
|
const pendingModules = new Map<RouteSection, Promise<RouteModule>>();
|
||||||
const backgroundPreloadOrder: RouteSection[] = [
|
const backgroundPreloadOrder: RouteSection[] = [
|
||||||
'monitor', 'tracks', 'history', 'statistics'
|
'monitor', 'vehicles', 'tracks', 'history', 'statistics', 'alerts', 'access', 'operations'
|
||||||
];
|
];
|
||||||
|
const backgroundPreloadBatchSize = 2;
|
||||||
|
|
||||||
function loadRoute(section: RouteSection) {
|
function loadRoute(section: RouteSection) {
|
||||||
const pending = pendingModules.get(section);
|
const pending = pendingModules.get(section);
|
||||||
@@ -84,11 +85,9 @@ export function scheduleIdleRoutePreloads({
|
|||||||
|
|
||||||
const warmNext = () => {
|
const warmNext = () => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const section = queue.shift();
|
const batch = queue.splice(0, backgroundPreloadBatchSize);
|
||||||
if (!section) return;
|
if (!batch.length) return;
|
||||||
void Promise.resolve()
|
void Promise.allSettled(batch.map((section) => Promise.resolve().then(() => preload(`/${section}`))))
|
||||||
.then(() => preload(`/${section}`))
|
|
||||||
.catch(() => undefined)
|
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (!cancelled) cancelScheduled = schedule(warmNext);
|
if (!cancelled) cancelScheduled = schedule(warmNext);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ The default `test` command is now the production V2 suite. The old `App.tsx` int
|
|||||||
|
|
||||||
React documents that `lazy` caches the import promise and propagates a rejected module load to the nearest Error Boundary; this is why a route-level boundary is required rather than a loading fallback alone: <https://react.dev/reference/react/lazy>. React's Suspense documentation also distinguishes a loading fallback from error handling: <https://react.dev/reference/react/Suspense>.
|
React documents that `lazy` caches the import promise and propagates a rejected module load to the nearest Error Boundary; this is why a route-level boundary is required rather than a loading fallback alone: <https://react.dev/reference/react/lazy>. React's Suspense documentation also distinguishes a loading fallback from error handling: <https://react.dev/reference/react/Suspense>.
|
||||||
|
|
||||||
|
The first continuity pass only warmed Monitor, Tracks, History and Statistics in the background. A production cold-cache audit with 450 ms simulated latency still measured roughly 1.66 seconds for `/monitor` to `/access`, leaving Vehicles, Alerts, Access and Operations dependent on click-time loading. The idle queue now covers all eight route sections in bounded batches of two. It still skips `saveData`, `slow-2g` and `2g` connections, while pointer/focus/down intent preloading remains the fastest path for an immediately selected destination. Unit coverage proves every inactive route is queued and that cleanup cancels the next idle batch.
|
||||||
|
|
||||||
The navigation and progressive-loading direction follows mature observability systems: persistent global controls, collapsible sections and loading only the content needed for the current task. Elastic documents these dashboard interaction and panel-organization patterns here: <https://www.elastic.co/docs/explore-analyze/dashboards/using> and <https://www.elastic.co/docs/explore-analyze/dashboards/arrange-panels>.
|
The navigation and progressive-loading direction follows mature observability systems: persistent global controls, collapsible sections and loading only the content needed for the current task. Elastic documents these dashboard interaction and panel-organization patterns here: <https://www.elastic.co/docs/explore-analyze/dashboards/using> and <https://www.elastic.co/docs/explore-analyze/dashboards/arrange-panels>.
|
||||||
|
|
||||||
### Remaining audit queue
|
### Remaining audit queue
|
||||||
|
|||||||
Reference in New Issue
Block a user