Skip to main content
A cold first present pays a network round-trip while the user waits. Prefetching warms the cache ahead of time, so when the user reaches the placement, it opens instantly. You can prefetch on demand, declare placements to warm at launch, and check readiness before presenting. All prefetch APIs are no-ops when cachingEnabled is false (there would be nothing to retain).

Prefetch on demand

prefetch(placementIds) warms each placement’s flow JSON (memory + disk cache) and custom fonts by running the same resolve path a present would. A later presentPlacement for a warmed placement then hits the cache with no network. It runs all placements concurrently and never rejects: it resolves to a Record<string, PrefetchOutcome> so you can see exactly what is warm.
const outcomes = await FlowPilot.prefetch(['onboarding', 'paywall_main']);

for (const [key, outcome] of Object.entries(outcomes)) {
  switch (outcome.state.type) {
    case 'warmed':
      console.log(`${key} is warm and ready`);
      break;
    case 'noFlow':
      console.log(`${key}: backend has nothing to show`);
      break;
    case 'failed':
      console.warn(`${key} failed to warm`, outcome.state.error);
      break;
  }
}
Input keys are de-duplicated. When cachingEnabled is false, the call is skipped and returns {}.

Warming images too

By default prefetch warms only JSON and fonts. Pass { warmMedia: true } to also warm images into the image cache so the flow paints instantly on arrival. The screen window is bounded by prefetchMediaStrategy (first screen by default).
await FlowPilot.prefetch(['paywall_main'], { warmMedia: true });

PrefetchOutcome

interface PrefetchOutcome {
  placementKey: string;
  state:
    | { type: 'warmed' }              // a presentable flow is cached and ready
    | { type: 'noFlow' }              // resolve ok, backend has nothing to show
    | { type: 'failed'; error: Error }; // resolve failed; nothing warmed
  mediaWarmed: boolean;              // first/all-screen images warmed
  isWarmed: boolean;                 // convenience: true when state is 'warmed'
}

Prefetch on launch

Declare placements to warm automatically, once, right after configure(...). Warming runs in the background and never blocks startup. When 2+ placements are declared, they are resolved in a single batch round-trip (falling back to per-placement resolves if the batch endpoint is unavailable).
FlowPilot.configure({
  apiKey: 'fp_live_...',
  appId: 'your-app-id',
  prefetchOnLaunch: ['onboarding', 'paywall_main'],
  prefetchMediaStrategy: 'firstScreen', // 'none' | 'firstScreen' (default) | 'allScreens'
});
Launch prefetch derives media warming from prefetchMediaStrategy, so it warms first-screen images by default (unlike a bare prefetch([...]), which warms no media unless you ask).
Warmed flows only live as long as their freshness TTL (the resolve response’s cacheTtlSeconds, falling back to the environment default). The development environment uses a 0 TTL by default, so a warmed entry expires immediately and launch prefetch is effectively a no-op there. Use staging or production to see the benefit. See Caching.
Cached flows are keyed by an identity fingerprint (a hash of the user ID and targeting attributes), so a flow warmed for one user is never served to another after an identity change.

prefetchMediaStrategy

Governs how aggressively launch prefetch warms images, and bounds the screen window when an explicit prefetch(..., { warmMedia: true }) opts into media.
StrategyWarms
noneFlow JSON + fonts only; no images.
firstScreen (default)Also the first screen’s and persistent-zone images.
allScreensAlso every screen’s images (best for short flows).

Readiness check

isPlacementReady(placementKey) answers whether a presentable flow is currently available for the current identity. It runs through the same cache-populating path as prefetch, so the resolve it may perform is not wasted: a presentable flow is left warm, and a following presentPlacement hits the cache (Tier 0) instead of resolving again. The ready-then-present sequence therefore costs at most one network resolve.
if (await FlowPilot.isPlacementReady('onboarding')) {
  await FlowPilot.presentPlacement('onboarding'); // no second round-trip
}
It returns true when any tier can present (fresh cache, live resolve, stale cache, or bundled default) and false when the backend has nothing to show or every tier fails. It never throws.

Example: warm right after sign-in

A common pattern is to warm the next flow the moment the user signs in, so the first paywall opens instantly.
async function onSignedIn() {
  // Fire and forget: never rejects, never blocks.
  void FlowPilot.prefetch(['paywall_main'], { warmMedia: true });
}

Common mistakes

  • Prefetching with caching disabled. prefetch and prefetchOnLaunch are no-ops when cachingEnabled is false. Enable caching to use them.
  • Expecting launch prefetch to help in development. The 0 default TTL expires the warm entry immediately. Test prefetch against staging or production.
  • Awaiting prefetch on a critical path. It is best-effort and can take a round-trip. Fire it in the background (void FlowPilot.prefetch(...)) and let the present hit whatever is warm.
  • Assuming warmMedia defaults on. A bare prefetch([...]) warms JSON and fonts only. Pass { warmMedia: true } for images.

Troubleshooting

  • prefetch returns noFlow for a placement. The backend resolved the placement but has nothing to show (no published flow, or targeting excludes this user). Expected; not an error.
  • First present still shows a spinner after prefetch. The TTL may have expired (especially in development), the identity changed since warming, or warmMedia was off so images still load on arrival. Check logLevel: 'debug' output for the delivery source.
  • isPlacementReady returns false unexpectedly. Offline with no cache and no bundled default, or the backend has no flow for this user. Ship a bundled default for guaranteed readiness.