Skip to content

perf(web): replace the Lottie spinner with CSS and defer lottie-web - #14568

Open
dylanjeffers wants to merge 1 commit into
perf/lazy-wallet-modalsfrom
perf/css-loading-spinner
Open

perf(web): replace the Lottie spinner with CSS and defer lottie-web#14568
dylanjeffers wants to merge 1 commit into
perf/lazy-wallet-modalsfrom
perf/css-loading-spinner

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

4 of 4 in a stack reducing the web entry chunk. Based on #14567. Includes the CI budget that locks in the whole stack.

What

lottie-web is a ~613 KB animation runtime. A loading spinner is the one component that cannot be lazily loaded — it's what renders while things load — so LoadingSpinner pinned the whole runtime into the entry chunk for every visitor.

The spinner is now CSS/SVG.

The replacement isn't eyeballed

Read from the Lottie source: a 32px circle with a 6px round-capped stroke whose dash grows 0→100 over 1s while rotating 410°, then shrinks back over 2s while rotating 719°. The CSS reproduces that with stroke-dasharray/dashoffset plus a rotation, and was compared against the original frozen at six phases across the cycle. It also honours prefers-reduced-motion, which the Lottie version did not.

⚠️ The svg > g > path structure is load-bearing

Roughly ten stylesheets recolour the spinner with .someClass g path { stroke: … } selectors written against the Lottie output:

GatedConditionsPill, Artwork, GiantTrackTile, DownloadRow, BottomButtons,
DesktopSearchBar, SearchBar, LoadingSpinner, PurchaseContentFormFooter, LibraryPage

Emitting a <circle> would have silently broken spinner colours across the app. Please keep this in mind if editing the markup.

Replacing the spinner alone gained nothing

Twelve other components imported lottie-react directly (play bar, search bar, notification reactions, animated buttons, cover photo). They now route through one LazyLottie wrapper.

  • Type-only imports like LottieRefCurrentProps are preserved as import type so they erase at build time.
  • lottieRef is an ordinary prop rather than a React ref, so it forwards through the boundary unchanged; callers keep their existing if (lottieRef.current) guards.

CI bundle budget

Also points bundlesize at the client entry chunk. It previously checked only the SSR server chunks at 30 kB, so the bundle every user downloads had no budget at all — which is how it reached 8.3 MB unnoticed. The builds artifact already contained build-production; the job just never looked at it.

Budget: 1600 kB gzip against a current 1.49 MB, so it can only ratchet down.

The job is gated on if: github.ref == 'refs/heads/main', so it reports after merge rather than blocking a PR. Worth revisiting separately — that's a PR-blocking policy decision.

Known tradeoff

PlayButton renders only the Lottie with no icon fallback, so it's briefly empty between mount and the chunk arriving (~270ms locally, longer on a slow first visit). Giving LazyLottie a fallback prop and passing static harmony icons would close that; the right per-button icon is a design call.

Stack result

Entry chunk raw gzip
baseline 8,333 KB 2,473 KB
after all 4 PRs 5,618 KB 1,521 KB
−32.6% −38.5%

🤖 Generated with Claude Code

lottie-web is a ~613 KB animation runtime. A loading spinner is the one
component that cannot be lazily loaded -- it is what renders *while* things
load -- so LoadingSpinner pinned the whole runtime into the entry chunk for
every visitor.

The spinner is now CSS/SVG. The replacement is not eyeballed: the Lottie source
is a 32px circle with a 6px round-capped stroke whose dash grows 0 -> 100 over
1s while rotating 410deg, then shrinks back over 2s while rotating 719deg. The
CSS reproduces that with stroke-dasharray/dashoffset plus a rotation, and was
compared against the original frozen at six phases across the cycle. It also
honours prefers-reduced-motion, which the Lottie version did not.

The `svg > g > path` structure is deliberate and load-bearing: roughly ten
stylesheets recolour the spinner with `.someClass g path { stroke: ... }`
selectors written against the Lottie output. Emitting a <circle> would have
silently broken spinner colours across the app.

Replacing the spinner alone would have gained nothing -- twelve other components
imported lottie-react directly (play bar, search bar, notification reactions,
animated buttons, cover photo). They now route through one LazyLottie wrapper.
Type-only imports such as LottieRefCurrentProps are preserved as `import type`
so they erase at build time. `lottieRef` is an ordinary prop rather than a React
ref, so it forwards through the boundary unchanged; callers keep their existing
`if (lottieRef.current)` guards.

Also points the CI bundlesize check at the client entry chunk. It previously
checked only the SSR *server* chunks at 30 kB, so the bundle every user
downloads had no budget at all -- which is how it reached 8.3 MB unnoticed. The
builds artifact already contained build-production; the job just never looked at
it. Budget set at 1600 kB gzip against a current 1.49 MB so it can only ratchet
down. Note the job is gated on `github.ref == 'refs/heads/main'`, so it reports
after merge rather than blocking a PR -- worth revisiting separately.

KNOWN TRADEOFF: PlayButton renders only the Lottie with no icon fallback, so it
is briefly empty between mount and the chunk arriving (~270ms locally, longer on
a slow first visit). Giving LazyLottie a `fallback` prop and passing static
harmony icons would close that; the right per-button icon is a design call.

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

changeset-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7510630

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

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