From a28fab0ff33cf9ba2eaa2ec5659020e03a65cb4d Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 14 Mar 2026 15:31:13 -0700 Subject: [PATCH] optimize skeleton animation (#1832) --- src/shared/components/skeleton/skeleton.tsx | 70 +++++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/src/shared/components/skeleton/skeleton.tsx b/src/shared/components/skeleton/skeleton.tsx index a7c5d039b..e1c654d15 100644 --- a/src/shared/components/skeleton/skeleton.tsx +++ b/src/shared/components/skeleton/skeleton.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { type CSSProperties, memo } from 'react'; +import { type CSSProperties, memo, useEffect, useRef, useState } from 'react'; import styles from './skeleton.module.css'; @@ -32,6 +32,64 @@ export function BaseSkeleton({ style, width, }: SkeletonProps) { + const containerRef = useRef(null); + const [isInViewport, setIsInViewport] = useState(false); + const [isDocumentVisible, setIsDocumentVisible] = useState( + typeof document === 'undefined' ? true : document.visibilityState === 'visible', + ); + + useEffect(() => { + if (!enableAnimation || typeof document === 'undefined') { + return; + } + + const handleVisibilityChange = () => { + setIsDocumentVisible(document.visibilityState === 'visible'); + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [enableAnimation]); + + useEffect(() => { + if (!enableAnimation) { + setIsInViewport(false); + + return; + } + + const element = containerRef.current; + + if (!element) { + return; + } + + if (typeof IntersectionObserver === 'undefined') { + setIsInViewport(true); + + return; + } + + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + setIsInViewport(Boolean(entry?.isIntersecting)); + }, + { threshold: 0.01 }, + ); + + observer.observe(element); + + return () => { + observer.disconnect(); + }; + }, [enableAnimation, count, inline, isCentered, direction]); + + const shouldAnimate = enableAnimation && isDocumentVisible && isInViewport; + const skeletonStyle: CSSProperties = { ...style, ...(baseColor && { ['--base-color' as string]: baseColor }), @@ -49,19 +107,23 @@ export function BaseSkeleton({ }); const skeletonClasses = clsx(styles.skeleton, className, { - [styles.animated]: enableAnimation, + [styles.animated]: shouldAnimate, }); if (count <= 1) { return ( -
+
); } return ( -
+
{Array.from({ length: count }, (_, i) => (
))}