Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions packages/harmony/src/components/loading-spinner/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -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<IconProps, 'size' | 'color'>

/**
* 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 (
Expand All @@ -21,7 +50,31 @@ const LoadingSpinner = (props: LoadingSpinnerProps) => {
})}
{...rest}
>
<Lottie loop autoplay animationData={loadingSpinner} />
<svg
viewBox='0 0 48 48'
css={{
width: '100%',
height: '100%',
animation: `${rotate} 1.4s linear infinite`,
'@media (prefers-reduced-motion: reduce)': { animation: 'none' }
}}
>
<g>
<path
d='M24 8a16 16 0 1 0 0 32a16 16 0 1 0 0-32'
fill='none'
strokeWidth={6}
strokeLinecap='round'
css={{
animation: `${sweep} 2.1s ease-in-out infinite`,
'@media (prefers-reduced-motion: reduce)': {
animation: 'none',
strokeDasharray: `75 ${CIRCUMFERENCE}`
}
}}
/>
</g>
</svg>
</Flex>
)
}
Expand Down
5 changes: 5 additions & 0 deletions packages/web/bundlesize.prod.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/components/cover-photo/CoverPhoto.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
25 changes: 21 additions & 4 deletions packages/web/src/components/loading-spinner/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={cn(styles.container, className)} role='progressbar'>
<Lottie loop autoplay animationData={loadingSpinner} />
<svg className={styles.spinner} viewBox='0 0 48 48'>
<g>
<path
className={styles.arc}
d='M24 8a16 16 0 1 0 0 32a16 16 0 1 0 0-32'
fill='none'
strokeWidth={6}
strokeLinecap='round'
/>
</g>
</svg>
</div>
)
}
Expand Down
29 changes: 29 additions & 0 deletions packages/web/src/components/lottie/LazyLottie.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof LottieComponent>

export const LazyLottie = (props: LottieProps) => (
<Suspense fallback={null}>
<Lottie {...props} />
</Suspense>
)

export default LazyLottie
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
3 changes: 2 additions & 1 deletion packages/web/src/components/play-bar/PlayButton.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/components/search-bar/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/components/track/mobile/TrackListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/pages/not-found-page/NotFoundPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Loading