Skip to content

Faster app startup: instant cached UI and fewer network round trips#68

Merged
aarikmudgal merged 3 commits into
owenselles:mainfrom
oliversluke:feature/faster-startup
Jul 11, 2026
Merged

Faster app startup: instant cached UI and fewer network round trips#68
aarikmudgal merged 3 commits into
owenselles:mainfrom
oliversluke:feature/faster-startup

Conversation

@oliversluke

Copy link
Copy Markdown
Contributor

Problem

On a cold launch the app shows spinners for several seconds before anything useful appears. The causes:

  • Nothing on the Home/Store screens was cached: the hero banner, "last played", and favorites all wait for the full catalog, which is up to 15 sequential GraphQL pages (cursor pagination can't be parallelized — each page needs the previous cursor).
  • /v2/serverInfo (vpcId) was fetched three times on every launch — the catalog, library, and subscription queries each fetched it independently, and each paid that round trip before starting its real work.
  • The subscription was never cached, so the resolution/FPS options and tier in Settings stayed on defaults until the network came back.

Changes

  • Catalog disk cache (JSON file in Caches — too large for tvOS UserDefaults): Home, Store, and "last played" render instantly on relaunch from the previous launch's data while the fresh catalog loads in the background.
  • Subscription cache (UserDefaults): resolution/FPS options and membership tier appear immediately, refreshed in the background.
  • vpcId resolved once per launch instead of three times, persisted across launches (it only changes if the account is migrated to another region), and revalidated in the background — removes a serial round trip from all three launch queries and from the every-foreground library refresh.
  • Public catalog fetched in parallel with the browse pagination instead of after it.
  • Browse page size 500 (automatic fallback to the long-proven 200 if the endpoint ever rejects it): cuts the serial round trips roughly 3×. This also lifts a silent cap — 200 items × 15 pages truncated the catalog at 3000 games; a full catalog is 5769 games on the test account.

Also includes the signaling double-continuation-resume crash fix from #62 (identical commit; it merges cleanly whichever PR lands first). It fixes a launch-time crash when both signaling candidates connect near-simultaneously, which testing this branch reproduced.

Testing

Device-tested (Apple TV): full 5769-game catalog loads, subscription (Ultimate tier) and library load correctly, relaunch renders Home/Library/Settings instantly from cache, and a full streaming session ran normally.

🤖 Generated with Claude Code

@aarikmudgal

aarikmudgal commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

@oliversluke I also mentioned this today in #66 Issue. So a very good step.
Tested, indeed looks good and much faster app UX.
Quick observations,

  • From code the 200-item fallback would runs for every type of error including authentication and network failures, not just when the server rejects the 500-item page size. Is this intended or we want to fallback on 200-item call only when 500-item call is explicitly rejected by the server?
  • There is also a possible duplicate library request during startup: the initial load() fetches it while the .active scene transition may call refreshLibrary() simultaneously.

oliversluke and others added 2 commits July 10, 2026 23:18
Cold launch waited on the full catalog fetch (up to 15 sequential
GraphQL pages) before Home could render, refetched /v2/serverInfo three
times in parallel, and never cached the subscription, so resolution and
tier appeared late.

- Cache the catalog as a JSON file in Caches (too large for tvOS
  UserDefaults): Home, Store, and last-played render instantly on
  relaunch while fresh data loads in the background
- Cache the subscription in UserDefaults: resolution/FPS options and
  tier show immediately
- Resolve the vpcId once per launch instead of three times, persist it
  across launches, and revalidate it in the background
- Fetch the public catalog in parallel with the browse pagination
- Browse with 500-item pages (fallback to 200 if rejected): fewer
  serial round trips, and it lifts the previous silent 3000-game
  catalog cap (device now loads the full 5769-game catalog)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oliversluke oliversluke force-pushed the feature/faster-startup branch from 1a5f92e to d282aef Compare July 10, 2026 21:18
Review feedback on the startup optimization:

- The 200-item retry ran for every error, including auth and network
  failures, doubling the request volume and masking the real error.
  browsePages now throws a typed HTTP-status error and the retry runs
  only for client-error statuses that plausibly mean the 500-item page
  was rejected (4xx except auth). A GraphQL-level rejection (HTTP 200
  with no data) previously produced an empty catalog without ever
  falling back — an empty result now triggers the retry too.
- The scene-activation library refresh also fires on cold launch,
  fetching the library a second time in parallel with load(). Refreshes
  are now skipped until the initial load has finished.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oliversluke

Copy link
Copy Markdown
Contributor Author

@aarikmudgal Both points were right — fixed in 508965f:

  • The fallback was indeed catch-all. browsePages now throws a typed HTTP-status error and the 200-item retry only runs for client-error statuses that plausibly mean the page size was rejected (4xx except auth); auth/network/server errors propagate immediately.
  • The scene-activation refresh does fire on cold launch too — refreshLibrary() is now skipped until the initial load() completes, so the library is fetched once at startup.

While in there I also found that a GraphQL-level rejection of the 500-item page (HTTP 200 with no data) would have produced an empty catalog without ever falling back — an empty result now triggers the 200 retry as well.

@aarikmudgal

Copy link
Copy Markdown
Collaborator

@oliversluke Looks good :) One remark, I just noticed that there is a bit of delay when we click on play for any game, before the game art is shown with the loading bar, I know that was part of another PR, but was just wondering if this is a bug and could be fixed easily? Otherwise later today I will test this PR once more and close it.

@oliversluke

Copy link
Copy Markdown
Contributor Author

@aarikmudgal Good catch, and thanks for testing. I traced it: the loading screen's full-bleed background is an AsyncImage loading the large ~1920px hero (heroImageUrl/heroBannerUrl), which is a different, uncached image from the 272px box art the grids show — so tapping Play kicks off a fresh network fetch and the background sits on Color.black until it arrives. The title and loading bar themselves render immediately; it's only the art behind them that lags.

Worth noting it's not caused by this PR — #68 caches catalog metadata (the game list), not hero images, so the behavior is identical on main. It shouldn't block closing/merging this one.

It's very fixable though: prefetching the hero image on card focus (warming URLCache) so it's ready by the time Play is pressed. I'll open a separate PR for that shortly.

@aarikmudgal aarikmudgal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested with FH6 and COD BO6

@aarikmudgal aarikmudgal merged commit 26a63ff into owenselles:main Jul 11, 2026
1 check passed
@oliversluke oliversluke deleted the feature/faster-startup branch July 11, 2026 11:04
@aarikmudgal aarikmudgal self-assigned this Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants