> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getflowpilot.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Prefetching and readiness

> Warm placements before you need them so the first present opens with no spinner, and check whether a placement is ready.

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 `resolveSession` (or `createSession`) 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.

```ts theme={null}
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).

```ts theme={null}
await FlowPilot.prefetch(['paywall_main'], { warmMedia: true });
```

### `PrefetchOutcome`

```ts theme={null}
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).

```ts theme={null}
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).

<Note>
  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](/expo-sdk/caching).
</Note>

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.

| Strategy                | Warms                                               |
| ----------------------- | --------------------------------------------------- |
| `none`                  | Flow JSON + fonts only; no images.                  |
| `firstScreen` (default) | Also the first screen's and persistent-zone images. |
| `allScreens`            | Also 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 `resolveSession` hits the cache (Tier 0) instead of resolving again. The ready-then-present sequence therefore costs at most one network resolve.

```tsx theme={null}
if (await FlowPilot.isPlacementReady('onboarding')) {
  const session = await FlowPilot.resolveSession('onboarding'); // no second round-trip
  setSession(session); // render <FlowPilotPresenter session={session} />
}
```

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.

```tsx theme={null}
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](/expo-sdk/offline-bundled-flows) for guaranteed readiness.

## Related pages

* [Caching](/expo-sdk/caching)
* [Media preloading](/expo-sdk/media-preloading)
* [Offline and bundled flows](/expo-sdk/offline-bundled-flows)
* [Presenting placements](/expo-sdk/presenting-placements)
* [Configuration](/expo-sdk/configuration)
