perf: Reduce homescreen load time by 56%#92
Conversation
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>
There was a problem hiding this comment.
💡 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".
IngrediCheck/Store/FamilyStore.swift
Outdated
|
|
||
| // MARK: - Cache | ||
|
|
||
| private let familyCacheKey = "cached_family_json" |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Addressed — cache key is now scoped to user ID: cached_family_json_{userId} (commit 0c84b5f). Additionally, clearCache() is called from resetLocalState() on sign-out.
| if stats == nil { | ||
| group.addTask { @MainActor in | ||
| await loadStats() | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| group.addTask { @MainActor in self.prefetchedStats = try? await self.webService.fetchStats() } | ||
| group.addTask { @MainActor in self.prefetchedFoodNotesSummary = try? await self.webService.fetchFoodNotesSummary()?.summary } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
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>
Summary
Result: Homescreen settles in ~1.6s (down from ~3.6s), verified on device with zero duplicate network calls.
Test plan
🤖 Generated with Claude Code