Skip to content

[Mobile] Faster cold-start splash + smaller cache blob + no image flash#14277

Open
raymondjacobson wants to merge 3 commits intomainfrom
mobile-perf-fixes
Open

[Mobile] Faster cold-start splash + smaller cache blob + no image flash#14277
raymondjacobson wants to merge 3 commits intomainfrom
mobile-perf-fixes

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

@raymondjacobson raymondjacobson commented May 8, 2026

Summary

Three targeted fixes based on screen recording analysis of the cold-start sequence.

1. Splash drops immediately (no longer gated on isRestoring)

The native splash was held until React Query finished restoring from AsyncStorage (~1.2 s on device). Account data arrives from the synchronous localStorage cache, so showHomeStack is true even during restoration — no sign-in flash. Removing the gate means the splash drops as soon as React is ready and the lineup skeleton (informative) is visible during hydration instead of a blank white screen.

2. Persisted cache blob narrowed to trending-only tracks/users

shouldDehydrateQuery previously let every track and user query through, so a long browsing session produced a large blob that made the AsyncStorage read + JSON.parse progressively slower on the next cold-start. The filter now limits track/user persistence to the IDs in the current trending week lineup plus the account user's owner records. A short-lived in-memory cache avoids re-querying the client on every entry in a single dehydration pass. Cache buster bumped to v3 to drop oversized blobs from prior builds.

3. Low-res → high-res image flash eliminated

When useImageSize had a 150×150 URL in cache but needed 480×480, it set imageUrl to the small URL then switched to the large URL once fetched. That source change triggered Image.tsx to reset opacity = 0 and re-fade — a visible flash on track pages, profile pages, and playlist pages.

useImageSize now sets imageUrl to the target URL optimistically and returns priorityLowResUrl separately. TrackImage, CollectionImage, and UserImage thread this through as priorityLowResSource on Artwork/Image, which renders it as a blurred backdrop while the high-res crossfades in — no opacity reset, no flash.

Test plan

  • Cold-start: splash should drop faster and skeleton should be visible during hydration
  • Trending screen: images should appear without flashing
  • Navigate to a track page: artwork should crossfade from blurred low-res to full-res (no flash)
  • Navigate to a profile: same crossfade behaviour
  • Navigate to a playlist: same crossfade behaviour
  • Verify on both iOS and Android

🤖 Generated with Claude Code

## Splash gate removed

The native splash was held until isRestoring=false (~1.2s on device).
Account data comes from the synchronous localStorage cache, so
showHomeStack is true even during React Query restoration — there is no
sign-in screen flash. Dropping the gate means the splash dismisses as
soon as React is ready, and the lineup skeleton (which is more
informative than a blank white screen) is visible during the
~1s hydration window instead.

## Persisted blob narrowed to trending-only tracks/users

query-persister.ts previously let every track and user query through
the allowlist regardless of how many the user had browsed. On a long
session this produced a large JSON blob that made the AsyncStorage
read + JSON.parse at next launch progressively slower.

shouldDehydrateQuery now caps track/user persistence to only the IDs
present in the trending week lineup (plus the account user's owner
data). A short-lived in-memory cache avoids re-querying the client
for every entry in a single dehydration pass. Cache buster bumped to
v3 to drop any oversized blobs from previous builds.

## Image low-res→high-res flash eliminated

When useImageSize had a smaller cached URL (e.g. 150x150) and needed
to load a larger one (e.g. 480x480), it set imageUrl to the small
URL then switched to the large URL once fetched. That source change
caused Image.tsx to reset opacity to 0 and re-fade — a visible flash
on track pages, profile pages, and playlist pages.

useImageSize now sets imageUrl to the target URL immediately
(optimistic, consistent with the no-cache path) and returns
priorityLowResUrl as a separate value. TrackImage, CollectionImage,
and UserImage thread this through as priorityLowResSource on Artwork/
Image, which the component uses as a blurred backdrop while the
high-res crossfades in — no opacity reset, no flash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 8, 2026

⚠️ No Changeset found

Latest commit: 64c2327

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@raymondjacobson raymondjacobson marked this pull request as ready for review May 8, 2026 01:39
@raymondjacobson raymondjacobson changed the title [Mobile] Fix lineup skeleton artwork size to match real tile (#14267) [Mobile] Faster cold-start splash + smaller cache blob + no image flash May 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

🌐 Web preview ready

Preview URL: https://audius-web-preview-pr-14277.audius.workers.dev

Unique preview for this PR (deployed from this branch).
Workflow run

raymondjacobson and others added 2 commits May 7, 2026 18:53
Dropping canDismiss={!isRestoring} caused the SectionList in
TrendingLineupSkeletons to measure at near-zero height on the first
visible frame, rendering only one tile at the bottom of an empty screen.

ScreenPrimaryContent gates on isScreenReady (InteractionManager +
200ms failsafe). That measurement previously happened under the splash
so the layout was settled before anything became visible. Without the
gate the first user-visible frame precedes the native layout pass.

The splash duration is addressed by the cache blob narrowing in this
branch — only trending tracks/users are persisted, so isRestoring
completes much faster than before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bumping to v3 dropped the old cache, making isRestoring complete
instantly (nothing to restore). With an empty cache the splash
starts fading before the SectionList measures its container,
producing the same 0-height blank-screen bug.

The buster bump was unnecessary: shouldDehydrateQuery only affects
writes, not reads. Old v2 caches have the same dehydration shape and
load correctly — they're just larger. The new write-time filter will
shrink the blob over subsequent sessions without disrupting existing
cache data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant