Skip to content

perf: load Solana, wallet and purchase code on demand - #14664

Merged
dylanjeffers merged 6 commits into
mainfrom
perf/crypto-lazy-barrels
Oct 13, 2026
Merged

dylanjeffers merged 6 commits into
mainfrom
perf/crypto-lazy-barrels

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

Part of moving crypto code off the app's startup path. This is the common/web/mobile half; #14661 (SDK), #14662 (hedgehog), #14663 (crypto shim) and #14660 (boot work) are the others.

Problem

Crypto libraries were statically reachable from the app root through shared barrels, not through sagas:

  • local-storage.ts → @audius/common/services → AudiusBackend.ts/solana.ts pulled in web3.js, spl-token, viem and Jupiter.
  • reducers.ts → the buy-usdc/purchase-content/ui index files, which re-export their sagas.
  • AppProviders → WagmiProvider.
  • registerNiceModals.ts statically registered the purchase, withdraw, wallet and rewards modals.

Changes

  1. Solana, Jupiter and viem load on demand (common).
    • AudiusBackend's Solana methods moved to solanaBackend.ts, which loads on first call. findAssociatedTokenAddress is now async; its 3 callers are updated.
    • audius-backend/solana is no longer re-exported from the services barrel.
    • WalletClient, the hedgehog Solana wallet service and the wallet/Jupiter/coin query hooks import web3.js, spl-token, tweetnacl, viem and Jupiter inside the async code that uses them.
    • isValidSolAddress now checks for a 32-byte base58 key instead of constructing a PublicKey.
  2. Crypto sagas start on demand (web + mobile).
    • A new lazySagas helper waits for a feature's first trigger action, dynamic-imports its sagas, spawns them, and replays the buffered actions.
    • Replays go to the saga middleware's channel, so reducers don't run twice.
    • Converted:
      • common: purchase-content, withdraw-USDC, Stripe onramp, buy-USDC (triggered by onrampOpened/startRecoveryIfNecessary)
      • mobile: token dashboard and wallet-connect
  3. No app-wide WagmiProvider (web).
    • useConnectExternalWallets was the only wagmi-context consumer; it now passes AppKit's config explicitly.
    • ReownAppKitModal calls reconnect() when it loads.
    • auth.ts and sign-out import @wagmi/core dynamically.
  4. 14 purchase/wallet/rewards modals registered lazily (web), using the existing registerLazyModal.
    • CoinflowPurchaseProtection now mounts when the browser is idle, in its own Suspense boundary. It used to share a boundary with <Outlet />, so page content waited on the Coinflow chunk.

Impact (web entry chunk)

Test plan

  • Typecheck: common, web, mobile, sdk (also checked combined with the other 4 branches)
  • common vitest (the 7 upload/selectors.test.ts failures also happen on main), web vitest, mobile jest 34/34
  • New lazySagas tests, including no double reducer run on replay
  • iOS production Metro bundle builds (dynamic imports resolve)
  • Prod build against prod API, signed out: /trending and /signin render with no app errors; a lazy modal opens; a dispatched trigger loads its sagas and handles the replayed action
  • Buy a premium track with USDC balance, with card (Stripe and Coinflow), and as a guest
  • Withdraw USDC to a Solana address and through Coinflow
  • External wallet sign-in; reload while connected (reconnects?); add/remove a linked wallet; sign out (disconnects?)
  • Fan club connect-wallet and the launchpad; send tokens; buy/sell swap; payout wallet modal
  • Claim rewards and vested coins
  • Mobile: token dashboard wallets, Phantom connect and sign

🤖 Generated with Claude Code

dylanjeffers and others added 5 commits October 12, 2026 15:05
The services barrel pulled AudiusBackend, audius-backend/solana and
WalletClient into the web entry chunk, and with them @solana/web3.js,
spl-token, buffer-layout and Jupiter. Query hooks in the api barrel did
the same.

- Move AudiusBackend's Solana methods into solanaBackend.ts, loaded on
  first call. findAssociatedTokenAddress is now async.
- Stop re-exporting audius-backend/solana from the services barrel;
  importers use the deep path or a dynamic import.
- WalletClient, the hedgehog Solana wallet service, wallet/jupiter/coin
  query hooks and the userbank/coinflow/mint recovery hooks import
  web3.js, spl-token, tweetnacl, viem and Jupiter inside the async code
  that needs them.
- Split the Solana instruction helpers out of jupiter/utils into
  solanaUtils; useSwapCoins loads the swap orchestrator on demand.
- isValidSolAddress checks for a 32 byte base58 key instead of
  constructing a PublicKey.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add lazySagas, which waits for the first action a feature's sagas
handle, imports the saga module, starts it and replays the buffered
actions. Replays go straight onto the saga middleware's channel
(exposed to sagas as the `sagaChannel` context) so reducers don't run
twice.

- purchaseContentSagas, withdrawUSDCSagas and stripeModalUISagas from
  @audius/common/store now load their real sagas on the first matching
  action, so the web and mobile roots are unchanged.
- buyUSDCSagas loads right after boot rather than on demand because it
  runs the purchase recovery check on page load.
- Mobile token dashboard and wallet-connect sagas load on first use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wagmi was in the entry chunk only for a placeholder config and the root
WagmiProvider. The one hook that reads wagmi context,
useConnectExternalWallets, now passes AppKit's config explicitly, and
ReownAppKitModal reconnects a persisted wallet when it loads (what the
provider's Hydrate used to do). AppProviders still loads AppKit at
startup when a wallet connection is persisted. auth.ts and sign-out
import @wagmi/core dynamically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
These NiceModal modals were imported eagerly by registerNiceModals and
pulled Solana and purchase code into the entry chunk. Register them with
registerLazyModal like the AppKit/Coinflow modals.

Also mount CoinflowPurchaseProtection when the browser is idle, in its
own Suspense boundary. It shared the boundary around <Outlet />, so page
content waited on the Coinflow chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recovery no longer runs on page load (#14660), and startRecoveryIfNecessary
is itself a trigger, so the sagas can wait for an onramp or recovery
request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4bb6847

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

dylanjeffers added a commit that referenced this pull request Oct 13, 2026
…unk (#14667)

The non-crypto half of the web bundle audit. The crypto half is
#14660–#14664.

## Impact (web entry chunk, prod build)
| | min | gzip |
|---|---|---|
| main | 5,627 KB | 1,526 KB |
| this PR, entry | 3,763 KB | 1,113 KB |
| this PR, entry + new vendor chunk | 4,103 KB | 1,222 KB |

## Changes (one commit each)
1. **Lazy Optimizely.** `@optimizely/optimizely-sdk` loads inside
`createOptimizelyClient` (~357 KB raw).
2. **Lazy queue popover.** It loads on first open and is prefetched on
hover, which takes the atlaskit drag-and-drop library out of the entry
(~225 KB raw).
3. **Lazy notification panel and mobile-web notification page.** The
panel mounts on the first hover or open of the bell (~105 KB raw). The
notification list is no longer fetched at boot.
4. **`SubPage` enum moved to `settings-page/types.ts`.** `WebPlayer`
imported it from the mobile `SettingsPage` and dragged in the whole page
(~78 KB raw).
5. **redux-persist uses localStorage instead of localforage** (~96 KB
raw). Each key is copied once from localforage, behind a per-key done
flag, and localforage is dynamically imported only for that copy. Search
history and library filters carry over; tested by seeding IndexedDB.
6. **SDK: tus-js-client and Node file-type load on demand** (~209 KB
raw). Uploads also stop if aborted while tus loads. A changeset is
included.
7. **SDK: the storage node list is fetched on first
`getSelectedNode()`** instead of in the constructor. That's one fewer
request on every web and mobile boot. Concurrent callers share the
request, and a failed request is retried.
8. **Trending fetches only the visible lineup** (desktop and mobile
web). The other time ranges load on hover or press of their filter, and
Underground when its tab opens. 4 fewer requests on load.
9. **The 25 remaining NiceModals use `registerLazyModal`**, appended as
a separate block. Expect a small conflict with #14664 in
`registerNiceModals.ts`: drop the side-effect imports and keep both lazy
blocks.
10. **svgo for SVGs.** It uses mobile's preset and keeps ids, colors,
`viewBox` and geometry. The icon SVGs in the entry go from about 385 to
247 KB. Pixel-diffing all 279 SVGs at 8x showed only sub-pixel edge
changes. Looser settings visibly moved logos with embedded images, so
they're off.
11. **Vendor chunk** for react, react-dom, react-router, redux,
react-redux, redux-saga and query-core. It stays cached across deploys
and is modulepreloaded. SSR is unchanged.
12. **Preconnect** to the API host and experiments.audius.co.

## Test plan
- [x] tsc: web, sdk, mobile. Web vitest (36 files), sdk vitest (21
files), with new tests for the storage migration and the lazy storage
node fetch.
- [x] Prod build smoke test signed out: no app errors; the queue
popover, trending tabs and the Share modal work; no `/health_check` on
load; only the week trending request on load.
- [ ] Signed in: notification panel (bell hover/open,
`?openNotifications`), the mobile-web notifications page, mobile-web
settings sub-pages
- [ ] Search history and library filters survive the upgrade
(localforage → localStorage)
- [ ] Track and image upload on web and mobile (lazy tus, lazy storage
node selection)
- [ ] Open a sample of lazy modals
- [ ] Feature flags still apply

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
dylanjeffers added a commit that referenced this pull request Oct 13, 2026
Mobile startup work from the bundle/startup audit. Everything here is
JS-only, so it ships via OTA. The common and `web/src/common` changes
also help web.

## Changes
- **Drawers and screens load on first open (mobile).** The 54 drawer
components in `Drawers.tsx` were object-literal values, which defeats
inline requires. They're now thunks called only when a drawer is
visible. Every stack screen except `AppTabs` uses `getComponent`, plus
`UpdateStack`, `ResetPassword` and `OAuthScreen` in `RootScreen`. About
680 of 7,526 modules (4.7 MB unminified source) are now reachable only
through these lazy paths.
- **Account load no longer waits on identity (common).**
- `recordIPIfNotRecent` and the chat block-list fetches are spawned
(detached), so they don't hold `fetchAccountAsync` or its sign-up
callers. Block lists wait 5s; only chat send checks read them.
- The timezone is sent only when it or the user changes, or weekly.
Sign-in and sign-up still send it.
- `hasTracks` comes from the account user's `track_count` instead of a
`getTracksByUserHandle` request.
- **Device token registration is skipped when unchanged (mobile).** The
last token and user are cached for 1 week. Still registers on a new
install, a different user, or a rotated token. Sign-out and turning off
push clear the cache.
- **Chat connect and chat list wait for startup interactions (mobile).**
Capped at 3s. The unread count still fetches immediately.
- **Challenge polling (web + mobile).**
- Fast polling (the remote interval) runs only on the rewards page and
for 2 minutes after challenge activity: a listen, a claim, the rewards
screen focusing, or a challenge drawer opening.
  - Otherwise it polls at max(5 min, remote interval).
- It **pauses** while the tab is hidden or the app is backgrounded.
Before, native polled every 30s even during background playback. A poll
that came due meanwhile runs on return.
- The foreground/background lifecycle actions moved to
`web/src/common/store/lifecycle/actions.ts`.
- Production `CHALLENGE_REFRESH_INTERVAL_MS` was also raised to 300000
in Optimizely (2026-10-12).
- **Smaller persisted query cache (mobile).** Only trending lineup
tracks, their owners and the account user are persisted, not every
viewed entity. This speeds the restore the splash waits on, and the 5s
save.
- **Offline download metadata is read 8 files at a time** instead of one
by one.
- **Optimizely SDK is required when the client is created**, not at
module load.

## Not done
Moving Android Cast init off the main thread. `react-native-google-cast`
calls the synchronous `CastContext.getSharedInstance()` internally, so
async init could return null and crash. It needs a library patch and a
native release.

## Merge notes
- Touches `RootScreen.tsx` and `account/sagas.ts` near #14660's edits,
without overlapping them.
- Doesn't touch `metro.config.js`, `index.js` or `store/sagas.ts`
(#14664).

## Test plan
- [x] tsc: mobile, common, web. eslint on changed files. Common account
saga test. Mobile jest (9 suites). Minified iOS production bundle
builds.
- [ ] Open drawers: overflow, add to playlist, premium purchase, add
cash, Coinflow, send tokens, queue, feed and trending filters
- [ ] Deep links: track, profile, chat. Navigate to settings, upload,
edit track, OAuth, reset password.
- [ ] Sign up (normal and guest checkout) completes without a delay
- [ ] IP and timezone requests fire once, not on every reload
- [ ] Artist dashboard and upload entry points still show for artists
- [ ] Push: a fresh install receives push. Sign out, sign in as another
user, and push goes to the new user.
- [ ] Unread chat badge appears on launch; new messages arrive live
- [ ] Background playback makes no challenge requests. Completing a
listen streak updates the claimable badge within about 15s. Claiming
shows confetti.
- [ ] Cold start offline: trending and account paint before the splash
clears; the offline downloads list fills in
- [ ] Feature flags resolve on launch, including the override screen

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/web/src/app/registerNiceModals.ts
@dylanjeffers
dylanjeffers merged commit d9d5c83 into main Oct 13, 2026
18 checks passed
@dylanjeffers
dylanjeffers deleted the perf/crypto-lazy-barrels branch October 13, 2026 22:23
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