Skip to content

Commit e516fd1

Browse files
authored
feat(nx-dev): make animations more performant (#27757)
It updates the animations to make them more performant, using variaous techniques to reduce the number of renders.
1 parent 963a1c5 commit e516fd1

File tree

3 files changed

+79
-97
lines changed

3 files changed

+79
-97
lines changed
Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { animate, useInView } from 'framer-motion';
2-
import { useEffect, useRef, useState } from 'react';
1+
import {
2+
animate,
3+
motion,
4+
useInView,
5+
useMotionValue,
6+
useTransform,
7+
} from 'framer-motion';
8+
import { memo, useEffect, useRef } from 'react';
39
import { usePrefersReducedMotion } from './prefers-reduced-motion';
410

511
/**
@@ -13,7 +19,7 @@ import { usePrefersReducedMotion } from './prefers-reduced-motion';
1319
*
1420
* @return {JSX.Element} - The JSX element representing the animated value with suffix.
1521
*/
16-
export function AnimateValue({
22+
function AnimateValueEngine({
1723
num,
1824
once = false,
1925
suffix,
@@ -23,31 +29,32 @@ export function AnimateValue({
2329
once?: boolean;
2430
suffix: string;
2531
decimals?: number;
26-
}) {
32+
}): JSX.Element {
2733
const ref = useRef<HTMLSpanElement | null>(null);
28-
const [isComplete, setIsComplete] = useState<boolean>(false);
2934
const isInView = useInView(ref);
3035
const shouldReduceMotion = usePrefersReducedMotion();
36+
const motionValue = useMotionValue(0);
37+
const formattedValue = useTransform(motionValue, (latest) =>
38+
latest.toFixed(decimals)
39+
);
3140

3241
useEffect(() => {
33-
if (!isInView) return;
34-
if (isComplete && once) return;
42+
if (!isInView || (once && motionValue.get() === num)) return;
3543

36-
animate(0, num, {
44+
animate(motionValue, num, {
3745
duration: shouldReduceMotion ? 0 : 2.5,
38-
onUpdate(value) {
39-
if (!ref.current) return;
40-
41-
ref.current.textContent = value.toFixed(decimals);
42-
},
46+
type: 'tween',
4347
});
44-
setIsComplete(true);
45-
}, [num, decimals, isInView, once]);
48+
}, [num, isInView, once, motionValue, shouldReduceMotion]);
4649

4750
return (
48-
<span>
49-
<span ref={ref}></span>
50-
<span>{suffix}</span>
51-
</span>
51+
<>
52+
<span ref={ref}>
53+
<motion.span>{formattedValue}</motion.span>
54+
</span>
55+
{suffix}
56+
</>
5257
);
5358
}
59+
60+
export const AnimateValue = memo(AnimateValueEngine);
Lines changed: 32 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
'use client';
2-
import {
3-
AnimatePresence,
4-
motion,
5-
useInView,
6-
useWillChange,
7-
Variants,
8-
} from 'framer-motion';
9-
import { ReactNode, useRef } from 'react';
2+
import { motion, useInView, useWillChange, Variants } from 'framer-motion';
3+
import { ReactNode, useRef, useMemo } from 'react';
104
import { usePrefersReducedMotion } from './prefers-reduced-motion';
115

126
interface BlurFadeProps {
@@ -25,23 +19,6 @@ interface BlurFadeProps {
2519
once?: boolean;
2620
}
2721

28-
/**
29-
* Applies a blur fade effect to its children based on the scroll position.
30-
*
31-
* @param {Object} props - The component props.
32-
* @param {React.ReactNode} props.children - The child elements to apply the effect to.
33-
* @param {string} [props.className] - The CSS class to apply to the component.
34-
* @param {Object} [props.variant] - The animation variants to apply.
35-
* @param {number} [props.duration=0.4] - The duration of the animation in seconds.
36-
* @param {number} [props.delay=0] - The delay before starting the animation in seconds.
37-
* @param {number} [props.yOffset=6] - The distance the element moves on the Y-axis during the animation.
38-
* @param {boolean} [props.inView=false] - Specifies whether the animation should trigger when the element is in view.
39-
* @param {string} [props.inViewMargin='-50px'] - The margin to consider when checking if the element is in view.
40-
* @param {string} [props.blur='5px'] - The amount of blur to apply to the element during the animation.
41-
* @param {boolean} [props.once=false] - Specifies whether the animation should only trigger once.
42-
*
43-
* @return {React.ReactNode} The component with the blur fade effect applied.
44-
*/
4522
export function BlurFade({
4623
children,
4724
className,
@@ -57,36 +34,41 @@ export function BlurFade({
5734
const willChange = useWillChange();
5835
const ref = useRef(null);
5936
const inViewResult = useInView(ref, { once, margin: inViewMargin });
60-
const isInView = !inView || inViewResult;
61-
const defaultVariants: Variants = {
62-
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
63-
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
64-
};
65-
const combinedVariants = variant || defaultVariants;
37+
const isInView = useMemo(
38+
() => !inView || inViewResult,
39+
[inView, inViewResult]
40+
);
41+
42+
const variants = useMemo((): Variants => {
43+
return (
44+
variant || {
45+
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
46+
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
47+
}
48+
);
49+
}, [variant, yOffset, blur]);
6650

6751
const shouldReduceMotion = usePrefersReducedMotion();
52+
6853
if (shouldReduceMotion) {
69-
return;
54+
return <div className={className}>{children}</div>;
7055
}
7156

7257
return (
73-
<AnimatePresence>
74-
<motion.div
75-
ref={ref}
76-
initial="hidden"
77-
animate={isInView ? 'visible' : 'hidden'}
78-
exit="hidden"
79-
variants={combinedVariants}
80-
style={{ willChange }}
81-
transition={{
82-
delay: 0.04 + delay,
83-
duration,
84-
ease: 'easeOut',
85-
}}
86-
className={className}
87-
>
88-
{children}
89-
</motion.div>
90-
</AnimatePresence>
58+
<motion.div
59+
ref={ref}
60+
initial="hidden"
61+
animate={isInView ? 'visible' : 'hidden'}
62+
variants={variants}
63+
style={{ willChange: willChange }}
64+
transition={{
65+
delay: 0.04 + delay,
66+
duration,
67+
ease: 'easeOut',
68+
}}
69+
className={className}
70+
>
71+
{children}
72+
</motion.div>
9173
);
9274
}
Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
1-
import { useInView } from 'framer-motion';
2-
import { ComponentRef, ReactNode, useEffect, useRef } from 'react';
1+
import { motion, useInView, useMotionValue, useTransform } from 'framer-motion';
2+
import { ComponentRef, ReactNode, useEffect, useRef, useCallback } from 'react';
33
import { cx } from '@nx/nx-dev/ui-primitives';
44

5-
/**
6-
* Resizes the text to fit within its container.
7-
*
8-
* @param {Object} props - The component's properties.
9-
* @param {ReactNode} props.children - The content to be displayed within the component.
10-
* @param {string} [props.className=''] - The additional className to be applied to the component.
11-
*
12-
* @return {JSX.Element} - The rendered component.
13-
*/
145
export function FitText({
156
children,
167
className = '',
@@ -21,23 +12,14 @@ export function FitText({
2112
const containerRef = useRef<ComponentRef<'div'>>(null);
2213
const textRef = useRef<ComponentRef<'span'>>(null);
2314
const isInView = useInView(containerRef);
15+
const fontSize = useMotionValue(16);
16+
const scaledFontSize = useTransform(fontSize, (size) => `${size}px`);
2417

25-
useEffect(() => {
26-
if (!isInView) return;
27-
resizeText();
28-
window.addEventListener('resize', resizeText);
29-
return () => {
30-
window.removeEventListener('resize', resizeText);
31-
};
32-
}, [isInView, children]);
33-
34-
const resizeText = () => {
18+
const resizeText = useCallback(() => {
3519
const container = containerRef.current;
3620
const text = textRef.current;
3721

38-
if (!container || !text) {
39-
return;
40-
}
22+
if (!container || !text) return;
4123

4224
const containerWidth = container.offsetWidth;
4325
let min = 1;
@@ -54,23 +36,34 @@ export function FitText({
5436
}
5537
}
5638

57-
text.style.fontSize = max + 'px';
58-
};
39+
fontSize.set(max);
40+
}, [fontSize]);
41+
42+
useEffect(() => {
43+
if (!isInView) return;
44+
resizeText();
45+
window.addEventListener('resize', resizeText);
46+
47+
return () => {
48+
window.removeEventListener('resize', resizeText);
49+
};
50+
}, [isInView, resizeText]);
5951

6052
return (
6153
<span
6254
className="relative grid h-full w-full grid-cols-1 place-items-center"
6355
ref={containerRef}
6456
>
65-
<span
57+
<motion.span
6658
className={cx(
6759
'transform whitespace-nowrap text-center font-bold',
6860
className
6961
)}
7062
ref={textRef}
63+
style={{ fontSize: scaledFontSize }}
7164
>
7265
{children}
73-
</span>
66+
</motion.span>
7467
</span>
7568
);
7669
}

0 commit comments

Comments
 (0)