Concurrent remote preload
preloadRemotes loads multiple remotes in parallel during browser idle time. Dedupes with loadRemoteEntryso a real navigation later returns instantly from cache. Use it for "the user will eventually open these remotes, but the first paint should not pay for them."
Hover prefetch vs. concurrent preload
Concurrent preload = eager — fires after first paint, loads several remotes during idle frames. Use for top-nav destinations whose chunks you know users will reach.
Preload all remotes after first paint
import { preloadRemotes } from '@jorvel/runtime';
window.addEventListener('load', () => {
preloadRemotes(
[
{ name: 'dashboard', entryUrl: '/jorvel/remotes/dashboard/remoteEntry.js' },
{ name: 'profile', entryUrl: '/jorvel/remotes/profile/remoteEntry.js' },
{ name: 'billing', entryUrl: '/jorvel/remotes/billing/remoteEntry.js' },
],
{ concurrency: 2, idle: true },
);
});Per-remote telemetry
preloadRemotes(remotes, {
concurrency: 3,
onResult: (r) => console.log(r.remote, r.ok, r.durationMs),
});Options
| Option | Default | Purpose |
|---|---|---|
concurrency | 3 | Max simultaneous loads. Higher saturates network sooner; lower keeps idle time available for the user. |
idle | true | Wrap each load in requestIdleCallback. Set false to start immediately. |
idleBudgetMs | 8 | Minimum idle time before starting work. Bump to 16 to wait for a full frame of headroom. |
onResult | — | Per-remote outcome callback. Receives { remote, ok, durationMs, error? }. |
signal | — | AbortSignal to cancel queued (not in-flight) loads. |
Result shape
interface PreloadResult {
remote: string;
entryUrl: string;
ok: boolean;
durationMs: number;
error?: unknown;
}
const results = await preloadRemotes(remotes, { concurrency: 2 });
const failed = results.filter((r) => !r.ok);
if (failed.length) observability.reportError(new Error('preload failures'), { failed });Combine with Service Worker
Preloaded remoteEntry.js responses flow through the Service Worker cache set by jorvel sw generate. Second-load cost drops to cache-hit. The combination is what makes route changes feel native after the first session — preload puts the bytes in memory, the SW puts them on disk.
Network-aware recipe
Skip preload on slow connections or data-saver mode. The navigator.connection API is only available on Blink-family browsers; treat undefined as "assume okay".
import { preloadRemotes } from '@jorvel/runtime';
if (typeof window !== 'undefined') {
const conn = (navigator as any).connection;
const saveData = conn?.saveData === true;
const slow = conn?.effectiveType === '2g' || conn?.effectiveType === 'slow-2g';
if (!saveData && !slow) {
window.addEventListener('load', () => {
preloadRemotes(REMOTES, {
concurrency: 2,
idle: true,
idleBudgetMs: 16, // wait for a full frame of headroom
onResult: (r) => observability.reportMetric({
name: 'jorvel.preload',
value: r.durationMs,
tags: { remote: r.remote, ok: String(r.ok) },
}),
});
});
}
}Route prediction
For larger apps it's worth being selective about which remotes to preload. A simple heuristic: rank by historical next-click probability from analytics, preload the top 2-3.
// Ranked by clickthrough from the current page.
const candidates = predictNextRoutes(currentPath); // your analytics call
const top = candidates.slice(0, 2);
preloadRemotes(top.map((p) => REMOTES_BY_PATH[p]).filter(Boolean), {
concurrency: 2,
idle: true,
});Cancellation
Pass an AbortSignal if you want to cancel queued work — useful when the user navigates before idle work completes. In-flight network requests are not aborted (the browser would refetch them anyway when the route is hit).
const controller = new AbortController();
preloadRemotes(REMOTES, { concurrency: 2, signal: controller.signal });
// On unload or route change:
controller.abort();Don't preload everything on mobile
When NOT to use preload
- One-page apps with sticky users. If 80% of sessions stay on one remote, preloading others is pure waste.
- Bandwidth-constrained users. Always gate behind
saveData+ effective-type checks. - Pages where LCP is > 2.5s. Fix LCP first; preload work on a slow first-paint just compounds the problem.
