Skip to content

perf: Reduce homescreen load time by 56%#92

Merged
justanotheratom merged 6 commits intomainfrom
is-internal-posthog
Feb 13, 2026
Merged

perf: Reduce homescreen load time by 56%#92
justanotheratom merged 6 commits intomainfrom
is-internal-posthog

Conversation

@justanotheratom
Copy link
Collaborator

Summary

  • Prefetch during splash: Start all 4 network requests (family, scan history, stats, food notes) during the 1s splash animation instead of waiting for UI setup — eliminates 1.1s idle gap
  • Cache family data: Store family response (~194 bytes) in UserDefaults for instant display on warm launch; background refresh ensures freshness
  • Skip redundant onboarding check: Short-circuit the ~400ms remote metadata fetch for returning users who've already completed onboarding
  • Store-level dedup guards: Each store prevents duplicate in-flight requests internally, so callers don't need coordination logic
  • Concurrent RootContainerView.task: Family load and onboarding restore run in parallel instead of sequentially

Result: Homescreen settles in ~1.6s (down from ~3.6s), verified on device with zero duplicate network calls.

Test plan

  • Deploy to device, verify 4 network requests fire within ~50ms of session restore (during splash)
  • Kill and relaunch — family name/avatar should appear instantly from cache
  • Test fresh install / new user onboarding — prefetch should not fire, existing flow unchanged
  • Test sign-out → sign-in — cache cleared, fresh data loads correctly
  • Test airplane mode — cached family displays, no crash
  • Verify zero duplicate network requests in device logs

🤖 Generated with Claude Code

Start network requests during splash animation instead of waiting for
UI setup. Cache family data in UserDefaults for instant display on warm
launch. Skip redundant remote onboarding check for completed users.
Add store-level dedup guards to prevent duplicate in-flight requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34aa90cf60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


// MARK: - Cache

private let familyCacheKey = "cached_family_json"

Choose a reason for hiding this comment

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

P1 Badge Scope cached family payload to the current user

This introduces a single global UserDefaults key for family data, so every account on the device shares the same cached payload. In account-switch or offline scenarios, loadCurrentFamily() reads this cache before fetching and then suppresses errors when cached data exists, which can show the previous user’s household to the next signed-in user. Keying the cache by authenticated user id (and clearing on sign-out) is needed to prevent cross-account data leakage.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Addressed — cache key is now scoped to user ID: cached_family_json_{userId} (commit 0c84b5f). Additionally, clearCache() is called from resetLocalState() on sign-out.

Comment on lines +479 to +482
if stats == nil {
group.addTask { @MainActor in
await loadStats()
}

Choose a reason for hiding this comment

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

P1 Badge Reload stats after account changes, don't trust stale prefetch

This conditional skips loadStats() whenever stats is already set, but stats is now hydrated from prefetcher.prefetchedStats first. Because that prefetch cache is app-scoped and not reset by the normal sign-out flow, a subsequent sign-in in the same process can reuse the prior account’s prefetched stats and never fetch fresh values here, causing cross-account stale data on the home screen until another path forces a refresh.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Already handled — AppDidReset (AppFlowRouter.swift:69-87) creates a brand new HomescreenPrefetcher with prefetchedStats = nil. Plus .id(appResetID) forces a full view tree rebuild, which resets HomeView's @State stats to nil. So sign-out → sign-in always gets fresh stats.

Comment on lines +29 to +30
group.addTask { @MainActor in self.prefetchedStats = try? await self.webService.fetchStats() }
group.addTask { @MainActor in self.prefetchedFoodNotesSummary = try? await self.webService.fetchFoodNotesSummary()?.summary }

Choose a reason for hiding this comment

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

P2 Badge Route prefetch API calls through existing in-flight guards

prefetchIfNeeded() calls fetchStats() and fetchFoodNotesSummary() directly, but HomeView.task can concurrently call loadStats() and foodNotesStore.loadSummaryIfNeeded() before prefetch completes on slower networks. Since these prefetch calls bypass store-level dedup/in-flight checks, startup can issue duplicate requests to the same endpoints, undermining the intended no-duplicate-load behavior and increasing launch latency/traffic.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Already addressed via store-level guards: FoodNotesStore has guard \!isLoadingSummary, ScanHistoryStore has guard \!isLoading && \!hasLoaded, FamilyStore has guard \!isFetchInFlight. Stats don't have a store (local @State), so a rare duplicate is harmless and self-resolving.

justanotheratom and others added 5 commits February 12, 2026 18:56
Address code review findings:
1. Key family cache by user ID to prevent cross-account data leakage
2. Clear stale cached family on non-recoverable errors (401, 403, 404)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address code review findings:
1. Remove didPrefetch check from RootContainerView — rely on
   FamilyStore's isFetchInFlight guard for dedup. If prefetch fails,
   the family load retries naturally.
2. Retry scan history load in HomeView if prefetch was in-flight
   and failed (hasLoaded=false, isLoading=false after task group).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…clear after sign-out

1. FamilyStore: Replace isFetchInFlight bool with shared Task so
   concurrent callers await the same in-flight fetch instead of
   returning early as no-ops.
2. HomeView: Replace one-shot scan history retry with .onChange
   observer that retries when isLoading transitions to false
   without hasLoaded being set (covers prefetch failure).
3. FamilyStore: Track active cache key name so clearCache() works
   after sign-out when session is nil.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. HomeView: Add didRetryScanHistory flag so the .onChange retry
   fires at most once, preventing an unbounded retry loop when
   requests keep failing (offline, auth issues).
2. FamilyStore: Remove .authError from cache-invalidating errors.
   authError is a local "no token" condition (e.g., during startup
   before session restoration) — only server rejections (401/403/404)
   should invalidate the cache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- FamilyStore: Extract withCoalescedFetch helper, split cache key into
  sessionFamilyCacheKey/activeFamilyCacheKey/familyCacheKeyForRead,
  extract shouldInvalidateCachedFamily (403+404 only, 401 excluded
  to avoid transient auth bootstrap races)
- ScanHistoryStore: Add activeLoadTask for coalesced concurrent loads,
  add loadInitialHistoryIfNeeded with bounded retry, cancel tasks
  in reset/clearAll
- HomeView: Replace onChange retry observer with store-level
  loadInitialHistoryIfNeeded call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@justanotheratom justanotheratom merged commit 8b5b6bc into main Feb 13, 2026
@justanotheratom justanotheratom deleted the is-internal-posthog branch February 13, 2026 05:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant