[Perf] Animated Counter: Avoid redundant rerenders
This commit is contained in:
parent
f1f776fff5
commit
90adfdf5e0
@ -1,72 +1,104 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, {
|
import React, { memo, useEffect, useMemo } from '../../lib/teact/teact';
|
||||||
useEffect, useMemo, useRef,
|
|
||||||
} from '../../lib/teact/teact';
|
|
||||||
import { getGlobal } from '../../global';
|
import { getGlobal } from '../../global';
|
||||||
|
|
||||||
import { selectCanAnimateInterface } from '../../global/selectors';
|
import { selectCanAnimateInterface } from '../../global/selectors';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { throttleWithTickEnd } from '../../util/schedulers';
|
||||||
|
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
|
|
||||||
import styles from './AnimatedCounter.module.scss';
|
import styles from './AnimatedCounter.module.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
text: string;
|
text: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ANIMATION_TIME = 200;
|
||||||
|
const MAX_SIMULTANEOUS_ANIMATIONS = 10;
|
||||||
|
|
||||||
|
let scheduledAnimationsCounter = 0;
|
||||||
|
|
||||||
|
const resetCounterOnTickEnd = throttleWithTickEnd(() => {
|
||||||
|
scheduledAnimationsCounter = 0;
|
||||||
|
});
|
||||||
|
|
||||||
const AnimatedCounter: FC<OwnProps> = ({
|
const AnimatedCounter: FC<OwnProps> = ({
|
||||||
text,
|
text,
|
||||||
className,
|
className,
|
||||||
|
isDisabled,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useOldLang();
|
const { isRtl } = useLang();
|
||||||
|
|
||||||
const prevTextRef = useRef<string>();
|
const prevText = usePrevious(text);
|
||||||
const [isAnimating, markAnimating, unmarkAnimating] = useFlag(false);
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
const shouldAnimate = selectCanAnimateInterface(getGlobal());
|
const shouldAnimate = scheduleAnimation(
|
||||||
|
!isDisabled && selectCanAnimateInterface(getGlobal()) && prevText !== undefined && prevText !== text,
|
||||||
|
);
|
||||||
|
|
||||||
const textElement = useMemo(() => {
|
const characters = useMemo(() => {
|
||||||
if (!shouldAnimate) {
|
return shouldAnimate ? renderAnimatedCharacters(text, prevText) : text;
|
||||||
return text;
|
}, [shouldAnimate, prevText, text]);
|
||||||
}
|
|
||||||
if (!isAnimating) {
|
|
||||||
return prevTextRef.current || text;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevText = prevTextRef.current;
|
|
||||||
|
|
||||||
const elements = [];
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
if (prevText && text[i] !== prevText[i]) {
|
|
||||||
elements.push(
|
|
||||||
<div className={styles.characterContainer}>
|
|
||||||
<div className={styles.character}>{text[i]}</div>
|
|
||||||
<div className={styles.characterOld} onAnimationEnd={unmarkAnimating}>{prevText[i]}</div>
|
|
||||||
<div className={styles.characterNew} onAnimationEnd={unmarkAnimating}>{text[i]}</div>
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
elements.push(<span>{text[i]}</span>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prevTextRef.current = text;
|
|
||||||
|
|
||||||
return elements;
|
|
||||||
}, [shouldAnimate, isAnimating, text]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
markAnimating();
|
if (!shouldAnimate) return undefined;
|
||||||
}, [text]);
|
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
forceUpdate();
|
||||||
|
}, ANIMATION_TIME);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [shouldAnimate, text]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={buildClassName(styles.root, className)} dir={lang.isRtl ? 'rtl' : undefined}>
|
<span className={buildClassName(className, !isDisabled && styles.root)} dir={isRtl ? 'rtl' : undefined}>
|
||||||
{textElement}
|
{characters}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AnimatedCounter;
|
export default memo(AnimatedCounter);
|
||||||
|
|
||||||
|
function scheduleAnimation(condition: boolean) {
|
||||||
|
if (!condition || scheduledAnimationsCounter >= MAX_SIMULTANEOUS_ANIMATIONS) return false;
|
||||||
|
|
||||||
|
if (scheduledAnimationsCounter === 0) {
|
||||||
|
resetCounterOnTickEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledAnimationsCounter++;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAnimatedCharacters(text: string, prevText?: string) {
|
||||||
|
const elements: React.ReactNode[] = [];
|
||||||
|
const textLength = text.length;
|
||||||
|
const prevTextLength = prevText?.length ?? 0;
|
||||||
|
|
||||||
|
for (let i = 0; i <= textLength; i++) {
|
||||||
|
const charIndex = textLength - i;
|
||||||
|
const prevTextCharIndex = prevTextLength - i;
|
||||||
|
|
||||||
|
if (prevText && prevTextCharIndex >= 0 && text[charIndex] !== prevText[prevTextCharIndex]) {
|
||||||
|
elements.unshift(
|
||||||
|
<div className={styles.characterContainer}>
|
||||||
|
<div className={styles.character}>{text[charIndex] ?? ''}</div>
|
||||||
|
<div className={styles.characterOld}>{prevText[prevTextCharIndex]}</div>
|
||||||
|
<div className={styles.characterNew}>{text[charIndex] ?? ''}</div>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
elements.unshift(<span>{text[charIndex] ?? ''}</span>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user