JORVEL

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

Prefetch = lazy — only fires on user intent (hover/focus). One remote at a time, near-zero idle cost.
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

ts
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

ts
preloadRemotes(remotes, {
  concurrency: 3,
  onResult: (r) => console.log(r.remote, r.ok, r.durationMs),
});

Options

OptionDefaultPurpose
concurrency3Max simultaneous loads. Higher saturates network sooner; lower keeps idle time available for the user.
idletrueWrap each load in requestIdleCallback. Set false to start immediately.
idleBudgetMs8Minimum idle time before starting work. Bump to 16 to wait for a full frame of headroom.
onResultPer-remote outcome callback. Receives { remote, ok, durationMs, error? }.
signalAbortSignal to cancel queued (not in-flight) loads.

Result shape

ts
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".

ts
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.

ts
// 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).

ts
const controller = new AbortController();
preloadRemotes(REMOTES, { concurrency: 2, signal: controller.signal });

// On unload or route change:
controller.abort();

Don't preload everything on mobile

Each remoteEntry.js plus its first chunk is ~10–40 KB gzipped. Preloading five remotes on a 3G connection costs ~300 KB and can push out LCP. Pick the top two or three by likelihood of next-navigation, and let hover-prefetch handle the long tail.

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.