Repository navigation
perf: runtime re-render, font and first-visit fixes for web and mobile - #14677
Merged
Merged
Conversation
|
Web shipped Avenir twice: harmony's avenir.css (~240 KB of base64, in the entry CSS) and public/fonts.css (the same ~240 KB again). Every first visit downloaded about 370 KB gzipped of font CSS, half of it render-blocking. - public/fonts.css now points at six woff2 files in public/fonts/avenir (the same bytes, decoded) with font-display: swap, so browsers only fetch the weights a page renders. - index.html preloads Medium (body), DemiBold and Bold (titles, headings, labels): about 90 KB. - A Vite plugin drops harmony's inlined Avenir from the web build. Other harmony consumers are unchanged. - The 2026 landing page's Dust Bucer hero font, which the hero waits on, loads as woff2 (284 KB otf -> 111 KB; outlines, kern and GPOS identical). Entry CSS: 379 KB / 212 KB gzip -> 137 KB / 27 KB gzip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmUrAQMioSmE4rsEgLPEQL
- The playback rate/position hooks move out of WebPlayer into a PlaybackPersistence leaf, so play/pause, track changes and the 1s podcast position updates no longer re-render every route, the nav and modals. - Position polling only runs while long-form content plays and selects just the long-form flag from the track. Position writes to localStorage are batched to one per 5s and flushed on pause, track change, pagehide and when the tab is hidden. - Desktop PlayBar and NowPlaying only re-render when the displayed second changes, and prev/next read the position from the player, so the hotkeys are no longer re-bound every 500ms. - The mobile-web PlayBar created a new interval after every render; the progress bar now owns its own interval and state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmUrAQMioSmE4rsEgLPEQL
PersistGate holds the whole app until redux-persist rehydrates. For any key not yet in localStorage, persistStorage dynamic-imported localforage and read IndexedDB to migrate it, which meant every first-time visitor waited on an extra chunk download and an IndexedDB open before anything rendered. Check indexedDB.databases() first: with no `localforage` database there is nothing to migrate, so mark the key migrated and return. Browsers without databases() keep the old path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmUrAQMioSmE4rsEgLPEQL
Lineups rebuilt every tile on each play/pause/skip because togglePlay depended on the playing state and current queue item, and the lineup hooks returned new trackIds arrays and loadNextPage functions on every render. - Web and mobile TrackLineup (and mobile library TracksTab) read playback state from the store when a tile is pressed instead of subscribing to it. Tiles already select their own active/playing state. - Lineup hooks memoize data and trackIds, and a new useLoadNextPage keeps loadNextPage stable between page loads, so the load-more IntersectionObserver isn't recreated on every render either. - Mobile lineup SectionList uses windowSize 11 instead of the default 21. - Library selectors use createSelector, so the library page no longer re-renders on every redux action; web useLibraryCollections builds its Set in useMemo. - useToggleTrack (explore tiles, chat unfurls) selects one boolean instead of the whole current track; usePlayTrack and the collection page no longer create a selector or callback per render/track change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmUrAQMioSmE4rsEgLPEQL
- useCurrentAccount: placeholderData re-read and JSON-parsed the cached account from localStorage in every observer on every render while the account loaded (and for the whole session when signed out). The parsed result is now reused until the stored strings change. - useTracks/useUsers/useCollections memoize their queries array, so tanstack doesn't re-hash and re-sync N observers on every render. - Track page lineup: the remix parent/remixes, more-by-artist and trending requests run in parallel instead of one after another (3 round trips to 1). Section order and dedupe are unchanged. - Track rank: trending IDs get a 30 minute staleTime instead of 5. - Profile page: an artist's reposts are only fetched once the Reposts tab opens. Until the profile is known, reposts still load in parallel. - Mobile: offline-downloads persist is throttled to 1s, and ChatScreen no longer renders every cached message on mount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmUrAQMioSmE4rsEgLPEQL
- AudioPlayer: starting playback ran makeTrackData over the entire queue, so a lineup or 1000-track playlist fired a HEAD request for every track's stream and 1000px artwork (about 2N requests), competing with the track that was buffering. Only the start track, the one before it and the next 2 are now checked against their mirrors; the rest, and appended tracks, go in with their primary URLs. If a primary URL fails when its track plays, the existing PlaybackError handler re-resolves it against the mirrors. Offline tracks still use local files and gated tracks are still signed. Queued tracks no longer reset the playing track's retry count. - AudioPlayer: the offline-availability selector, which runs on every dispatch, builds its map in a loop instead of spreading in a reduce. - useImageSize reads preloadImageFn from a ref, so callers passing an inline function (every mobile image) no longer re-run the resolve effect and prefetch again on each render. A cached larger image is only used up to 3.5x the requested width, so 150px thumbnails don't decode 1000px art. - The collapsed Now Playing drawer loads 480px artwork and switches to 1000px when opened. Feed collection tiles use 150px like track tiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmUrAQMioSmE4rsEgLPEQL
dylanjeffers
force-pushed
the
claude/clever-edison-u4bbxy
branch
from
October 21, 2026 23:07
e651ec6 to
c32651a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to the bundle/startup series (#14660–#14673). That series was about bundle size; this one is about runtime work on web and mobile: re-renders, request waterfalls, polling, fonts and image sizes. One commit per area. Everything is JS-only and OTA-safe.
1. Web: fonts
Web shipped Avenir twice as base64 CSS, about 240 KB each time: once through harmony's
avenir.cssin the entry CSS, and again inpublic/fonts.css.public/fonts.cssnow points at six woff2 files, the same bytes decoded from the base64, withfont-display: swap.index.htmlpreloads Medium, DemiBold and Bold, about 90 KB.src/.../avenir.cssanddist/harmony.css. Other harmony consumers are unchanged.kernandGPOSare identical; only hinting is dropped.fonts.cssgoes from 242 KB to 1.3 KB.2. Web: app shell and play bar
WebPlayerinto a leafPlaybackPersistence. Play/pause, track changes and the 1s podcast position ticks no longer re-render every route, the nav and the modals.pagehideand hidden tab.3. Web: first visit
PersistGateheld the whole app until a dynamiclocalforageimport and an IndexedDB read finished, for every new visitor.indexedDB.databases()first and skips localforage when that database doesn't exist.4. Web + mobile: lineups
togglePlay: reads playback state at press time, so play/pause/skip no longer rebuild every lineup tile. Tiles already select their own playing state.trackIds, and the newuseLoadNextPagekeepsloadNextPagestable. This also stops the load-more IntersectionObserver being recreated on every render.createSelector; the library page had been re-rendering on every redux action.useToggleTrack(explore tiles, chat unfurls) selects a single boolean.windowSize11.5. Shared data hooks
useCurrentAccount:placeholderDataused to re-parse the cached account from localStorage in every observer on every render while the account loaded, and for the whole session when signed out. The parsed result is now reused until the stored strings change.useTracks,useUsersanduseCollectionsmemoize theirqueriesarray.staleTimeinstead of 5 minutes.6. Mobile: audio queue and images
PlaybackErrorhandler re-resolves that track and skips the primary.useImageSize: readspreloadImageFnfrom a ref, so inline callers no longer re-run the effect and prefetch on every render. A cached larger image is used only up to 3.5× the requested width.Not done
useProfilePicturewiresonError, so the preload is the only mirror fallback for track and collection art.focusManager/onlineManageron mobile: the retryer pauses while unfocused, so a failed fetch during background playback or CarPlay would hang.useHotKeysthrottle: a throttle that actually applied would skippreventDefaulton dropped keydowns, so the page would scroll.Test plan
useImageSize, 34 passedApp-test.tsxfails to resolve~/api/tan-query/utils, and fails the same way on main.@font-faceleft in any CSS asset/handle/repostsdeep links🤖 Generated with Claude Code
https://claude.ai/code/session_01DmUrAQMioSmE4rsEgLPEQL