From a2e42620eae39f2a55b6f70b2f71e334476de06e Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Fri, 5 Jun 2026 10:56:51 -0700 Subject: [PATCH 1/2] fix(mobile): start track comments fetch on screen mount to cut load delay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment section on the mobile track screen only started fetching once CommentPreview mounted, which was gated behind (1) the track fetch, (2) the useUser(owner_id) fetch — which comments don't need — and (3) the ScreenSecondaryContent primary-content gate. That serial waterfall left comments loading noticeably after the rest of the page. Add usePrefetchTrackComments and call it at the top of TrackScreen so the comment-list query fires on mount, in parallel with the track/user fetch, as soon as a trackId is known (from route params when navigating by id, before the track even resolves). It keeps a live observer on the gcTime:0 query so the warmed data survives until the comment section mounts and reads it from cache. The existing skeleton then rarely flashes because data is usually already present by the time CommentPreview renders. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/prefetch-track-comments.md | 5 +++ .../tan-query/comments/useTrackComments.ts | 41 +++++++++++++++++-- .../src/screens/track-screen/TrackScreen.tsx | 17 +++++++- 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 .changeset/prefetch-track-comments.md diff --git a/.changeset/prefetch-track-comments.md b/.changeset/prefetch-track-comments.md new file mode 100644 index 00000000000..b020fc4e3c3 --- /dev/null +++ b/.changeset/prefetch-track-comments.md @@ -0,0 +1,5 @@ +--- +'@audius/common': patch +--- + +Add `usePrefetchTrackComments`, a hook that warms a track's comment list query as early as possible (e.g. on track screen mount, in parallel with the track fetch) so the comment section renders from cache instead of starting its own fetch only once it mounts. It keeps a live observer on the `gcTime: 0` comment-list query so the warmed data isn't evicted before the comment section mounts, and uses the default `Top` sort/page size to guarantee a cache hit. diff --git a/packages/common/src/api/tan-query/comments/useTrackComments.ts b/packages/common/src/api/tan-query/comments/useTrackComments.ts index a6872592c7d..593b2a8d63b 100644 --- a/packages/common/src/api/tan-query/comments/useTrackComments.ts +++ b/packages/common/src/api/tan-query/comments/useTrackComments.ts @@ -1,6 +1,9 @@ import { useEffect } from 'react' -import { Id } from '@audius/sdk' +import { + GetTrackCommentsSortMethodEnum as CommentSortMethod, + Id +} from '@audius/sdk' import { useInfiniteQuery, useIsMutating, @@ -28,7 +31,13 @@ export type GetCommentsByTrackArgs = { pageSize?: number } -export const useTrackComments = ( +/** + * Base infinite query for a track's root comment IDs. Keeps a live observer on + * the comment-list query (which uses `gcTime: 0`, so the data is evicted the + * moment no observer is mounted) and primes individual comment data into the + * cache. Shared by `useTrackComments` and `usePrefetchTrackComments`. + */ +const useTrackCommentsQuery = ( { trackId, sortMethod, @@ -39,10 +48,9 @@ export const useTrackComments = ( const { audiusSdk } = useQueryContext() const isMutating = useIsMutating() const queryClient = useQueryClient() - const dispatch = useDispatch() const { data: currentUserId } = useCurrentUserId() - const queryRes = useInfiniteQuery({ + return useInfiniteQuery({ initialPageParam: 0, getNextPageParam: (lastPage: ID[], pages) => { if (lastPage?.length < pageSize) return undefined @@ -78,6 +86,31 @@ export const useTrackComments = ( ...options, enabled: isMutating === 0 && options?.enabled !== false && !!trackId }) +} + +/** + * Warms the track comment list as early as possible (e.g. on track screen + * mount, in parallel with the track fetch) so the comment section renders with + * data already in cache instead of starting its fetch only once it mounts. + * + * Uses the default `Top` sort and page size to match what the comment section + * requests by default, ensuring a cache hit. Keeps a live observer mounted so + * the `gcTime: 0` query isn't evicted before the comment section mounts. + */ +export const usePrefetchTrackComments = (trackId: ID | null | undefined) => { + useTrackCommentsQuery( + { trackId: trackId as ID, sortMethod: CommentSortMethod.Top }, + { enabled: !!trackId } + ) +} + +export const useTrackComments = ( + args: GetCommentsByTrackArgs, + options?: QueryOptions +) => { + const dispatch = useDispatch() + + const queryRes = useTrackCommentsQuery(args, options) const { error, data: commentIds } = queryRes diff --git a/packages/mobile/src/screens/track-screen/TrackScreen.tsx b/packages/mobile/src/screens/track-screen/TrackScreen.tsx index 39476946c84..28fb19031ae 100644 --- a/packages/mobile/src/screens/track-screen/TrackScreen.tsx +++ b/packages/mobile/src/screens/track-screen/TrackScreen.tsx @@ -1,6 +1,10 @@ import { useRef } from 'react' -import { useTrackByParams, useUser } from '@audius/common/api' +import { + useTrackByParams, + usePrefetchTrackComments, + useUser +} from '@audius/common/api' import { Kind } from '@audius/common/models' import { reachabilitySelectors } from '@audius/common/store' import { makeStableUid } from '@audius/common/utils' @@ -34,6 +38,17 @@ export const TrackScreen = () => { const { data: fetchedTrack } = useTrackByParams(restParams) const track = fetchedTrack ?? searchTrack + // Kick off the comments fetch as early as possible — on mount, in parallel + // with the track/user fetch — so the comment section renders from cache + // instead of starting its own fetch only once it mounts (gated behind the + // user fetch and the secondary-content gate below). Uses the trackId from + // route params when available so it can fire before the track resolves. + const paramTrackId = + 'trackId' in restParams ? restParams.trackId : undefined + usePrefetchTrackComments( + track?.comments_disabled ? null : (paramTrackId ?? track?.track_id) + ) + const { data: user } = useUser(track?.owner_id) if (!track || !user) { From c4c3c1264c5e7bf8d557fdf224cf299e0754f327 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Fri, 5 Jun 2026 15:47:42 -0700 Subject: [PATCH 2/2] fix(mobile): start track page lineup fetch before the screen-ready gate The "more by / remixes / you might also like" lineup only started fetching once TrackScreenLineup mounted, which is gated behind ScreenSecondaryContent's screen-ready delay (InteractionManager + nav animation, capped at 200ms). Its data deps (hero track + owner handle) are already resolved by the time TrackScreen passes its `!track || !user` guard, so the fetch waits on the gate for no reason. Add usePrefetchTrackPageLineup and call it at the top of TrackScreen. Unlike comments the lineup can't fire from a bare trackId (its queryFn needs the hero track's genre/remix_of and the owner's handle, and `enabled` requires ownerHandle), but hoisting it above the secondary-content gate lets it start the instant those deps resolve, overlapping the push transition. It keeps a live observer so the warmed data is shared with TrackScreenLineup's own query. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/prefetch-track-comments.md | 2 +- .../tan-query/lineups/useTrackPageLineup.ts | 20 +++++++++++++++++++ .../src/screens/track-screen/TrackScreen.tsx | 12 ++++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.changeset/prefetch-track-comments.md b/.changeset/prefetch-track-comments.md index b020fc4e3c3..4b22872d60b 100644 --- a/.changeset/prefetch-track-comments.md +++ b/.changeset/prefetch-track-comments.md @@ -2,4 +2,4 @@ '@audius/common': patch --- -Add `usePrefetchTrackComments`, a hook that warms a track's comment list query as early as possible (e.g. on track screen mount, in parallel with the track fetch) so the comment section renders from cache instead of starting its own fetch only once it mounts. It keeps a live observer on the `gcTime: 0` comment-list query so the warmed data isn't evicted before the comment section mounts, and uses the default `Top` sort/page size to guarantee a cache hit. +Add `usePrefetchTrackComments` and `usePrefetchTrackPageLineup`, hooks that warm a track's comment list and "more by / remixes / you might also like" lineup as early as possible (e.g. on track screen mount) so those sections render from cache instead of starting their own fetch only once they mount. Each keeps a live observer so the warmed data isn't evicted before the section mounts. `usePrefetchTrackComments` can fire from a bare trackId; `usePrefetchTrackPageLineup` still depends on the hero track + owner handle but starts the instant those resolve rather than waiting for the mobile screen-ready/animation gate. diff --git a/packages/common/src/api/tan-query/lineups/useTrackPageLineup.ts b/packages/common/src/api/tan-query/lineups/useTrackPageLineup.ts index 850ec4d231b..5cd89c0f5b2 100644 --- a/packages/common/src/api/tan-query/lineups/useTrackPageLineup.ts +++ b/packages/common/src/api/tan-query/lineups/useTrackPageLineup.ts @@ -226,3 +226,23 @@ export const useTrackPageLineup = ( queryKey } } + +/** + * Warms the track page lineup ("more by", remixes, "you might also like") as + * early as possible so the lineup renders from cache instead of starting its + * fetch only once `TrackScreenLineup` mounts — which on mobile is gated behind + * the screen-ready / nav-animation delay in `ScreenSecondaryContent`. + * + * Unlike comments, this query genuinely depends on the hero track and its + * owner's handle (see the `enabled` guard in `useTrackPageLineup`), so it can't + * fire from a bare trackId before the track resolves. But hoisting it above the + * secondary content gate lets it fire the instant those deps are ready — + * overlapping the push transition instead of waiting for it to finish. Keeps a + * live observer so the warmed data is shared with `TrackScreenLineup`'s own + * query. + */ +export const usePrefetchTrackPageLineup = ( + trackId: ID | null | undefined +) => { + useTrackPageLineup({ trackId }) +} diff --git a/packages/mobile/src/screens/track-screen/TrackScreen.tsx b/packages/mobile/src/screens/track-screen/TrackScreen.tsx index 28fb19031ae..e49e5ef6f5b 100644 --- a/packages/mobile/src/screens/track-screen/TrackScreen.tsx +++ b/packages/mobile/src/screens/track-screen/TrackScreen.tsx @@ -3,6 +3,7 @@ import { useRef } from 'react' import { useTrackByParams, usePrefetchTrackComments, + usePrefetchTrackPageLineup, useUser } from '@audius/common/api' import { Kind } from '@audius/common/models' @@ -45,9 +46,14 @@ export const TrackScreen = () => { // route params when available so it can fire before the track resolves. const paramTrackId = 'trackId' in restParams ? restParams.trackId : undefined - usePrefetchTrackComments( - track?.comments_disabled ? null : (paramTrackId ?? track?.track_id) - ) + const trackId = paramTrackId ?? track?.track_id + usePrefetchTrackComments(track?.comments_disabled ? null : trackId) + + // Warm the "more by / remixes / you might also like" lineup too. Unlike + // comments it can't fire from the bare trackId (it needs the hero track + + // owner handle), but hoisting it here lets it start as soon as those resolve + // instead of waiting for the ScreenSecondaryContent screen-ready gate. + usePrefetchTrackPageLineup(trackId) const { data: user } = useUser(track?.owner_id)