diff --git a/packages/harmony/src/components/loading-spinner/LoadingSpinner.tsx b/packages/harmony/src/components/loading-spinner/LoadingSpinner.tsx index 0a5da0533e8..b22d17fa2d8 100644 --- a/packages/harmony/src/components/loading-spinner/LoadingSpinner.tsx +++ b/packages/harmony/src/components/loading-spinner/LoadingSpinner.tsx @@ -1,12 +1,41 @@ -import Lottie from 'lottie-react' - -import loadingSpinner from '~harmony/assets/animations/loadingSpinner.json' +import { keyframes } from '@emotion/react' import { IconProps } from '../icon' import { Flex, FlexProps } from '../layout/Flex' type LoadingSpinnerProps = FlexProps & Pick +/** + * Circumference of the r=16 arc (2πr ≈ 100.53), rounded up. Used as the dash + * gap so a single dash segment can sweep the whole circle. + */ +const CIRCUMFERENCE = 101 + +const rotate = keyframes` + to { transform: rotate(360deg); } +` + +/** + * Grow-then-shrink sweep, matching the Lottie animation this replaced: the arc + * grew from empty to full over ~1s, then shrank back to empty over ~2s, while + * rotating throughout. + */ +const sweep = keyframes` + 0% { stroke-dasharray: 1 ${CIRCUMFERENCE}; stroke-dashoffset: 0; } + 50% { stroke-dasharray: 75 ${CIRCUMFERENCE}; stroke-dashoffset: -18; } + 100% { stroke-dasharray: 1 ${CIRCUMFERENCE}; stroke-dashoffset: -${CIRCUMFERENCE - 1}; } +` + +/** + * Previously rendered a Lottie animation. `lottie-web` is a ~613 KB animation + * runtime, and a loading spinner is the one component that cannot be lazily + * loaded — it is what renders *while* things load — so it pinned the whole + * runtime into the entry chunk for every visitor. + * + * The `svg > g > path` structure is deliberate: ~10 stylesheets across the web + * app recolour the spinner with `.someClass g path { stroke: ... }` selectors + * written against the Lottie output. Keeping the shape keeps those working. + */ const LoadingSpinner = (props: LoadingSpinnerProps) => { const { size = 'l', color, ...rest } = props return ( @@ -21,7 +50,31 @@ const LoadingSpinner = (props: LoadingSpinnerProps) => { })} {...rest} > - + + + + + ) } diff --git a/packages/web/bundlesize.prod.config.json b/packages/web/bundlesize.prod.config.json index 971891cc95c..9567841738a 100644 --- a/packages/web/bundlesize.prod.config.json +++ b/packages/web/bundlesize.prod.config.json @@ -3,6 +3,11 @@ { "path": "./build-ssr-production/server/chunks/chunk-*.js", "maxSize": "30 kB" + }, + { + "path": "./build-production/assets/index-*.js", + "maxSize": "1600 kB", + "compression": "gzip" } ] } diff --git a/packages/web/src/components/animated-button/AnimatedButtonProvider.tsx b/packages/web/src/components/animated-button/AnimatedButtonProvider.tsx index b3e28933d8c..d6451762aac 100644 --- a/packages/web/src/components/animated-button/AnimatedButtonProvider.tsx +++ b/packages/web/src/components/animated-button/AnimatedButtonProvider.tsx @@ -9,9 +9,10 @@ import { import { useInstanceVar } from '@audius/common/hooks' import cn from 'classnames' -import Lottie, { LottieRefCurrentProps } from 'lottie-react' +import type { LottieRefCurrentProps } from 'lottie-react' import { SeoLink } from 'components/link' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import { applyThemeToLottie } from 'utils/lottieTheme' import { useLottieThemeColors } from 'utils/theme/theme' diff --git a/packages/web/src/components/cover-photo/CoverPhoto.tsx b/packages/web/src/components/cover-photo/CoverPhoto.tsx index 9ea18508adb..ae1625f04c5 100644 --- a/packages/web/src/components/cover-photo/CoverPhoto.tsx +++ b/packages/web/src/components/cover-photo/CoverPhoto.tsx @@ -4,11 +4,11 @@ import { imageCoverPhotoBlank } from '@audius/common/assets' import { WidthSizes } from '@audius/common/models' import { Nullable } from '@audius/common/utils' import cn from 'classnames' -import Lottie from 'lottie-react' import { FileWithPreview } from 'react-dropzone' import loadingSpinner from 'assets/animations/loadingSpinner.json' import ImageSelectionButton from 'components/image-selection/ImageSelectionButton' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import { useCoverPhoto } from 'hooks/useCoverPhoto' import styles from './CoverPhoto.module.css' diff --git a/packages/web/src/components/follow-artist-card/FollowArtistCard.tsx b/packages/web/src/components/follow-artist-card/FollowArtistCard.tsx index cd5d3076067..4b24674ea7d 100644 --- a/packages/web/src/components/follow-artist-card/FollowArtistCard.tsx +++ b/packages/web/src/components/follow-artist-card/FollowArtistCard.tsx @@ -18,12 +18,12 @@ import { useTheme } from '@audius/harmony' import { useField } from 'formik' -import Lottie from 'lottie-react' import { useDispatch } from 'react-redux' import { useHover } from 'react-use' import { make } from 'common/store/analytics/actions' import { Avatar } from 'components/avatar/Avatar' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import Skeleton from 'components/skeleton/Skeleton' import { useCoverPhoto } from 'hooks/useCoverPhoto' import { useMedia } from 'hooks/useMedia' diff --git a/packages/web/src/components/loading-spinner/LoadingSpinner.module.css b/packages/web/src/components/loading-spinner/LoadingSpinner.module.css index 8f0357a91ab..3a47bf49d8f 100644 --- a/packages/web/src/components/loading-spinner/LoadingSpinner.module.css +++ b/packages/web/src/components/loading-spinner/LoadingSpinner.module.css @@ -1,3 +1,49 @@ .container g path { stroke: var(--harmony-n-500); } + +.spinner { + width: 100%; + height: 100%; + animation: spinner-rotate 1.4s linear infinite; +} + +/* + * Grow-then-shrink sweep, matching the Lottie animation this replaced: the arc + * grew from empty to full over ~1s, then shrank back over ~2s, rotating + * throughout. 101 is the r=16 circumference (2πr ≈ 100.53) rounded up. + */ +.arc { + animation: spinner-sweep 2.1s ease-in-out infinite; +} + +@keyframes spinner-rotate { + to { + transform: rotate(360deg); + } +} + +@keyframes spinner-sweep { + 0% { + stroke-dasharray: 1 101; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 75 101; + stroke-dashoffset: -18; + } + 100% { + stroke-dasharray: 1 101; + stroke-dashoffset: -100; + } +} + +@media (prefers-reduced-motion: reduce) { + .spinner { + animation: none; + } + .arc { + animation: none; + stroke-dasharray: 75 101; + } +} diff --git a/packages/web/src/components/loading-spinner/LoadingSpinner.tsx b/packages/web/src/components/loading-spinner/LoadingSpinner.tsx index 101ad70903d..34e4f694bd0 100644 --- a/packages/web/src/components/loading-spinner/LoadingSpinner.tsx +++ b/packages/web/src/components/loading-spinner/LoadingSpinner.tsx @@ -1,18 +1,35 @@ import cn from 'classnames' -import Lottie from 'lottie-react' - -import loadingSpinner from 'assets/animations/loadingSpinner.json' import styles from './LoadingSpinner.module.css' type LoadingSpinnerProps = { className?: string } +/** + * Previously rendered a Lottie animation. `lottie-web` is a ~613 KB animation + * runtime, and a spinner is the one component that cannot be lazily loaded — + * it is what renders *while* things load — so it pinned the runtime into the + * entry chunk for every visitor. + * + * The `svg > g > path` structure is deliberate: ~10 stylesheets recolour the + * spinner with `.someClass g path { stroke: ... }` selectors written against + * the Lottie output. Keeping the shape keeps those working. + */ const LoadingSpinner = (props: LoadingSpinnerProps) => { const { className } = props return (
- + + + + +
) } diff --git a/packages/web/src/components/lottie/LazyLottie.tsx b/packages/web/src/components/lottie/LazyLottie.tsx new file mode 100644 index 00000000000..280a9432560 --- /dev/null +++ b/packages/web/src/components/lottie/LazyLottie.tsx @@ -0,0 +1,29 @@ +import { ComponentProps, lazy, Suspense } from 'react' + +import type LottieComponent from 'lottie-react' + +/** + * `lottie-react` / `lottie-web` is a ~613 KB animation runtime. Importing it + * statically anywhere in the eager graph pins it into the entry chunk for every + * visitor, and it was reachable from a dozen surfaces (play bar, search bar, + * notification reactions, animated buttons). + * + * None of those animations are needed before first paint, so the runtime loads + * on demand instead. Use this in place of a direct `lottie-react` import. + * + * Note on `lottieRef`: lottie-react takes it as an ordinary prop rather than a + * React ref, so it forwards through this boundary unchanged. It is populated + * once the chunk resolves, so callers that drive playback imperatively must + * keep their existing `if (lottieRef.current)` guards. + */ +const Lottie = lazy(() => import('lottie-react')) + +type LottieProps = ComponentProps + +export const LazyLottie = (props: LottieProps) => ( + + + +) + +export default LazyLottie diff --git a/packages/web/src/components/notification/Notification/components/Reaction/Reaction.tsx b/packages/web/src/components/notification/Notification/components/Reaction/Reaction.tsx index d1647efb07c..23006d0e810 100644 --- a/packages/web/src/components/notification/Notification/components/Reaction/Reaction.tsx +++ b/packages/web/src/components/notification/Notification/components/Reaction/Reaction.tsx @@ -7,7 +7,9 @@ import { } from 'react' import cn from 'classnames' -import Lottie, { LottieOptions, LottieRefCurrentProps } from 'lottie-react' +import type { LottieOptions, LottieRefCurrentProps } from 'lottie-react' + +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import styles from './Reaction.module.css' diff --git a/packages/web/src/components/play-bar/PlayButton.tsx b/packages/web/src/components/play-bar/PlayButton.tsx index 7a8bf5252c1..1ee93732da4 100644 --- a/packages/web/src/components/play-bar/PlayButton.tsx +++ b/packages/web/src/components/play-bar/PlayButton.tsx @@ -1,11 +1,12 @@ import { useState, useEffect, useRef, useCallback } from 'react' import cn from 'classnames' -import Lottie, { LottieRefCurrentProps } from 'lottie-react' +import type { LottieRefCurrentProps } from 'lottie-react' import pbIconPause from 'assets/animations/pbIconPause.json' import pbIconPlay from 'assets/animations/pbIconPlay.json' import pbLoadingSpinner from 'assets/animations/pbLoadingSpinner.json' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import styles from './PlayBarButton.module.css' diff --git a/packages/web/src/components/play-bar/repeat-button/RepeatButton.tsx b/packages/web/src/components/play-bar/repeat-button/RepeatButton.tsx index 652f44b0da7..9a7231d4b52 100644 --- a/packages/web/src/components/play-bar/repeat-button/RepeatButton.tsx +++ b/packages/web/src/components/play-bar/repeat-button/RepeatButton.tsx @@ -1,8 +1,9 @@ import { useState, useEffect, useCallback, useRef } from 'react' import cn from 'classnames' -import Lottie, { LottieRefCurrentProps } from 'lottie-react' +import type { LottieRefCurrentProps } from 'lottie-react' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import { useIsMobile } from 'hooks/useIsMobile' import { applyThemeToLottie } from 'utils/lottieTheme' import { useLottieThemeColors } from 'utils/theme/theme' diff --git a/packages/web/src/components/play-bar/shuffle-button/ShuffleButton.tsx b/packages/web/src/components/play-bar/shuffle-button/ShuffleButton.tsx index 2e6172f5879..f4b07672470 100644 --- a/packages/web/src/components/play-bar/shuffle-button/ShuffleButton.tsx +++ b/packages/web/src/components/play-bar/shuffle-button/ShuffleButton.tsx @@ -1,8 +1,9 @@ import { useState, useEffect, useCallback, useRef } from 'react' import cn from 'classnames' -import Lottie, { LottieRefCurrentProps } from 'lottie-react' +import type { LottieRefCurrentProps } from 'lottie-react' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import { useIsMobile } from 'hooks/useIsMobile' import { applyThemeToLottie } from 'utils/lottieTheme' import { useLottieThemeColors } from 'utils/theme/theme' diff --git a/packages/web/src/components/search-bar/SearchBar.tsx b/packages/web/src/components/search-bar/SearchBar.tsx index 4b34c6dafaa..d8e9ea77120 100644 --- a/packages/web/src/components/search-bar/SearchBar.tsx +++ b/packages/web/src/components/search-bar/SearchBar.tsx @@ -3,9 +3,9 @@ import { ChangeEvent, KeyboardEvent } from 'react' import { Status } from '@audius/common/models' import { IconSearch, Tooltip } from '@audius/harmony' import cn from 'classnames' -import Lottie from 'lottie-react' import loadingSpinner from 'assets/animations/loadingSpinner.json' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import styles from './SearchBar.module.css' diff --git a/packages/web/src/components/track/mobile/TrackListItem.tsx b/packages/web/src/components/track/mobile/TrackListItem.tsx index 7a32e9552a8..50dc351c47c 100644 --- a/packages/web/src/components/track/mobile/TrackListItem.tsx +++ b/packages/web/src/components/track/mobile/TrackListItem.tsx @@ -20,10 +20,10 @@ import { IconVisibilityHidden } from '@audius/harmony' import cn from 'classnames' -import Lottie from 'lottie-react' import loadingSpinner from 'assets/animations/loadingSpinner.json' import { SeoLink } from 'components/link' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import { TablePlayButton } from 'components/table/components/TablePlayButton' import { useTrackCoverArt } from 'hooks/useTrackCoverArt' diff --git a/packages/web/src/pages/not-found-page/NotFoundPage.tsx b/packages/web/src/pages/not-found-page/NotFoundPage.tsx index 57bd5c7a358..ff22573fd3e 100644 --- a/packages/web/src/pages/not-found-page/NotFoundPage.tsx +++ b/packages/web/src/pages/not-found-page/NotFoundPage.tsx @@ -5,12 +5,12 @@ import { route } from '@audius/common/utils' import { Button, isLightTheme } from '@audius/harmony' import { useTheme } from '@emotion/react' import cn from 'classnames' -import Lottie from 'lottie-react' import { Link } from 'react-router' import notFoundAnimation from 'assets/animations/404.json' import tiledBackground from 'assets/img/notFoundTiledBackround.png' import { useRecord, make } from 'common/store/analytics/actions' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import NavContext, { CenterPreset, RightPreset diff --git a/packages/web/src/pages/profile-page/components/mobile/UploadStub.tsx b/packages/web/src/pages/profile-page/components/mobile/UploadStub.tsx index 809a12b4088..5d12ecaa036 100644 --- a/packages/web/src/pages/profile-page/components/mobile/UploadStub.tsx +++ b/packages/web/src/pages/profile-page/components/mobile/UploadStub.tsx @@ -2,9 +2,9 @@ import { useState, useRef, useCallback } from 'react' import { IconCloudUpload as IconUpload } from '@audius/harmony' import cn from 'classnames' -import Lottie from 'lottie-react' import loadingSpinner from 'assets/animations/loadingSpinner.json' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import styles from './UploadStub.module.css' diff --git a/packages/web/src/pages/track-page/components/mobile/ActionButtonRow.tsx b/packages/web/src/pages/track-page/components/mobile/ActionButtonRow.tsx index 4d120f19858..82ca8a2dffa 100644 --- a/packages/web/src/pages/track-page/components/mobile/ActionButtonRow.tsx +++ b/packages/web/src/pages/track-page/components/mobile/ActionButtonRow.tsx @@ -6,12 +6,12 @@ import { IconPencil } from '@audius/harmony' import cn from 'classnames' -import Lottie from 'lottie-react' import loadingSpinner from 'assets/animations/loadingSpinner.json' import AnimatedIconButton, { AnimatedIconType } from 'components/animated-button/AnimatedIconButton' +import { LazyLottie as Lottie } from 'components/lottie/LazyLottie' import styles from './ActionButtonRow.module.css'