Message List: Optimizations (#4595)

This commit is contained in:
zubiden 2024-06-12 18:10:37 +02:00 committed by Alexander Zinchuk
parent 1978419e84
commit c04014b7a1
19 changed files with 101 additions and 38 deletions

View File

@ -16,6 +16,7 @@ import { IS_ELECTRON } from '../../util/windowEnvironment';
import useColorFilter from '../../hooks/stickers/useColorFilter';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import useFlag from '../../hooks/useFlag';
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck';
import useLastCallback from '../../hooks/useLastCallback';
import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../hooks/usePriorityPlaybackCheck';
@ -96,6 +97,16 @@ const AnimatedSticker: FC<OwnProps> = ({
const rgbColor = useRef<[number, number, number] | undefined>();
const shouldForceOnHeavyAnimation = forceAlways || forceOnHeavyAnimation;
// Delay initialization until heavy animation ends
const [
canInitialize, markCanInitialize, unmarkCanInitialize,
] = useFlag(!isHeavyAnimating() || shouldForceOnHeavyAnimation);
useHeavyAnimationCheck(unmarkCanInitialize, markCanInitialize, shouldForceOnHeavyAnimation);
useEffect(() => {
if (shouldForceOnHeavyAnimation) markCanInitialize();
}, [shouldForceOnHeavyAnimation]);
useSyncEffect(() => {
if (color && !shouldUseColorFilter) {
const { r, g, b } = hexToRgb(color);
@ -118,6 +129,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|| isUnmountedRef.current
|| !tgsUrl
|| (sharedCanvas && (!sharedCanvasCoords || !sharedCanvas.offsetWidth || !sharedCanvas.offsetHeight))
|| (isHeavyAnimating() && !shouldForceOnHeavyAnimation)
) {
return;
}
@ -154,12 +166,13 @@ const AnimatedSticker: FC<OwnProps> = ({
});
useEffect(() => {
if (!canInitialize) return;
if (getRLottie()) {
init();
} else {
ensureRLottie().then(init);
}
}, [init, tgsUrl, sharedCanvas, sharedCanvasCoords]);
}, [init, tgsUrl, sharedCanvas, sharedCanvasCoords, canInitialize]);
const throttledInit = useThrottledCallback(init, [init], THROTTLE_MS);
useSharedIntersectionObserver(sharedCanvas, throttledInit);
@ -239,7 +252,7 @@ const AnimatedSticker: FC<OwnProps> = ({
}
}, [playAnimation, animation, tgsUrl]);
useHeavyAnimationCheck(pauseAnimation, playAnimation, !playKey || forceAlways || forceOnHeavyAnimation);
useHeavyAnimationCheck(pauseAnimation, playAnimation, !playKey || shouldForceOnHeavyAnimation);
usePriorityPlaybackCheck(pauseAnimation, playAnimation, !playKey || forceAlways);
// Pausing frame may not happen in background,
// so we need to make sure it happens right after focusing,

View File

@ -120,7 +120,7 @@ const File: FC<OwnProps> = ({
{withThumb && (
<canvas
ref={thumbRef}
className={buildClassName('thumbnail', thumbClassNames)}
className={buildClassName('thumbnail', 'canvas-blur-setup', thumbClassNames)}
/>
)}
</div>

View File

@ -154,7 +154,7 @@ const GifButton: FC<OwnProps> = ({
{withThumb && (
<canvas
ref={thumbRef}
className="thumbnail"
className="thumbnail canvas-blur-setup"
// We need to always render to avoid blur re-calculation
style={isVideoReady ? 'display: none;' : undefined}
/>

View File

@ -57,7 +57,12 @@ const MediaSpoiler: FC<OwnProps> = ({
ref={ref}
onClick={withAnimation ? handleClick : undefined}
>
<canvas ref={canvasRef} className={styles.canvas} width={width} height={height} />
<canvas
ref={canvasRef}
className={buildClassName(styles.canvas, 'canvas-blur-setup')}
width={width}
height={height}
/>
<div className={styles.dots} />
</div>
);

View File

@ -119,7 +119,7 @@ const ProfilePhoto: FC<OwnProps> = ({
content = (
<>
{isBlurredThumb ? (
<canvas ref={blurredThumbCanvasRef} className="thumb" />
<canvas ref={blurredThumbCanvasRef} className="thumb canvas-blur-setup" />
) : (
<img src={avatarBlobUrl} draggable={false} className="thumb" alt="" />
)}

View File

@ -9,7 +9,7 @@ import { getStickerPreviewHash } from '../../global/helpers';
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import * as mediaLoader from '../../util/mediaLoader';
import { IS_ANDROID, IS_WEBM_SUPPORTED } from '../../util/windowEnvironment';
import { IS_WEBM_SUPPORTED } from '../../util/windowEnvironment';
import useColorFilter from '../../hooks/stickers/useColorFilter';
import useCoordsInSharedCanvas from '../../hooks/useCoordsInSharedCanvas';
@ -115,8 +115,8 @@ const StickerView: FC<OwnProps> = ({
const fullMediaData = useMedia(fullMediaHash, !shouldLoad || shouldSkipFullMedia);
// If Lottie data is loaded we will only render thumb if it's good enough (from preview)
const [isPlayerReady, markPlayerReady] = useFlag(Boolean(isLottie && fullMediaData && !previewMediaData));
// Delay mounting on Android until heavy animation ends
const [isReadyToMount, markReadyToMount, unmarkReadyToMount] = useFlag(!IS_ANDROID || !isHeavyAnimating());
// Delay mounting until heavy animation ends
const [isReadyToMount, markReadyToMount, unmarkReadyToMount] = useFlag(!isHeavyAnimating());
useHeavyAnimationCheck(unmarkReadyToMount, markReadyToMount, isReadyToMount);
const isFullMediaReady = isReadyToMount && fullMediaData && (isStatic || isPlayerReady);

View File

@ -9,6 +9,9 @@ import { preloadImage } from '../../../util/files';
import { REM } from '../helpers/mediaDimensions';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useFlag from '../../../hooks/useFlag';
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia';
@ -76,6 +79,10 @@ const EmojiIconBackground = ({
const lang = useLang();
// Delay mounting until heavy animation ends
const [canUpdate, markCanUpdate, unmarkCanUpdate] = useFlag(!isHeavyAnimating());
useHeavyAnimationCheck(unmarkCanUpdate, markCanUpdate);
const { customEmoji } = useCustomEmoji(emojiDocumentId);
const previewMediaHash = customEmoji ? getStickerPreviewHash(customEmoji.id) : undefined;
const previewUrl = useMedia(previewMediaHash);
@ -83,10 +90,10 @@ const EmojiIconBackground = ({
const customColor = useDynamicColorListener(containerRef);
useEffect(() => {
if (!previewUrl) return;
if (!previewUrl || !canUpdate) return;
preloadImage(previewUrl).then(setEmojiImage);
}, [previewUrl]);
}, [previewUrl, canUpdate]);
const updateCanvas = useLastCallback(() => {
const canvas = canvasRef.current;
@ -122,13 +129,15 @@ const EmojiIconBackground = ({
context.restore();
});
useEffect(() => {
useEffectWithPrevDeps(([prevEmojiImage, prevLangRtl, prevCustomColor]) => {
// No need to trigger update if only `canUpdate` changed
if (emojiImage === prevEmojiImage && lang.isRtl === prevLangRtl && customColor === prevCustomColor) return;
updateCanvas();
}, [emojiImage, lang.isRtl, customColor]);
}, [emojiImage, lang.isRtl, customColor, canUpdate]);
const updateCanvasSize = useLastCallback((parentWidth: number, parentHeight: number) => {
const canvas = canvasRef.current;
if (!canvas) return;
if (!canvas || isHeavyAnimating()) return;
canvas.width = parentWidth * dpr;
canvas.height = parentHeight * dpr;
@ -147,18 +156,19 @@ const EmojiIconBackground = ({
});
});
useResizeObserver(containerRef, handleResize);
useResizeObserver(containerRef, handleResize, !canUpdate);
useEffect(() => {
useEffectWithPrevDeps(([prevDpr]) => {
if (dpr === prevDpr) return;
const container = containerRef.current;
if (!container) return;
if (!container || !canUpdate) return;
const { width, height } = container.getBoundingClientRect();
requestMutation(() => {
updateCanvasSize(width, height);
});
}, [dpr]);
}, [dpr, canUpdate]);
return (
<div className={buildClassName(styles.root, className)} ref={containerRef}>

View File

@ -36,6 +36,7 @@ type OwnProps = {
shouldPause?: boolean;
shouldLoop?: boolean;
loopLimit?: number;
shouldDelayInit?: boolean;
observeIntersection?: ObserveFn;
};
@ -66,6 +67,7 @@ const ReactionAnimatedEmoji = ({
shouldPause,
shouldLoop,
loopLimit,
shouldDelayInit,
observeIntersection,
}: OwnProps & StateProps) => {
const { stopActiveReaction } = getActions();
@ -167,7 +169,7 @@ const ReactionAnimatedEmoji = ({
size={size}
noPlay={shouldPause}
loopLimit={loopLimit}
forceAlways
forceAlways={!shouldDelayInit}
observeIntersectionForPlaying={observeIntersection}
/>
)}
@ -179,7 +181,7 @@ const ReactionAnimatedEmoji = ({
tgsUrl={mediaDataCenterIcon}
play={isIntersecting && !shouldPause}
noLoop={!shouldLoop}
forceAlways
forceAlways={!shouldDelayInit}
onLoad={markAnimationLoaded}
onEnded={unmarkAnimationLoaded}
/>
@ -193,7 +195,7 @@ const ReactionAnimatedEmoji = ({
tgsUrl={mediaDataEffect}
play={isIntersecting}
noLoop
forceAlways
forceAlways={!shouldDelayInit}
onEnded={handleEnded}
/>
{isCustom && !assignedEffectId && isIntersecting && (

View File

@ -83,7 +83,9 @@ function BaseStory({
className={fullClassName}
onClick={isConnected ? handleClick : undefined}
>
{!isExpired && isPreview && <canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />}
{!isExpired && isPreview && (
<canvas ref={blurredBackgroundRef} className="thumbnail canvas-blur-setup blurred-bg" />
)}
{shouldRender && (
<>
<img

View File

@ -972,6 +972,7 @@ const Message: FC<OwnProps & StateProps> = ({
noRecentReactors={isChannel}
tags={tags}
isCurrentUserPremium={isPremium}
getIsMessageListReady={getIsMessageListReady}
/>
);
}
@ -1475,6 +1476,7 @@ const Message: FC<OwnProps & StateProps> = ({
observeIntersection={observeIntersectionForPlaying}
noRecentReactors={isChannel}
tags={tags}
getIsMessageListReady={getIsMessageListReady}
/>
)}
</div>

View File

@ -199,7 +199,9 @@ const Photo: FC<OwnProps> = ({
style={style}
onClick={isUploading ? undefined : handleClick}
>
{withBlurredBackground && <canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />}
{withBlurredBackground && (
<canvas ref={blurredBackgroundRef} className="thumbnail canvas-blur-setup blurred-bg" />
)}
<img
src={fullMediaData}
className={buildClassName('full-media', withBlurredBackground && 'with-blurred-bg')}
@ -208,7 +210,10 @@ const Photo: FC<OwnProps> = ({
draggable={!isProtected}
/>
{withThumb && (
<canvas ref={thumbRef} className={buildClassName('thumbnail', thumbClassNames)} />
<canvas
ref={thumbRef}
className={buildClassName('thumbnail', !noThumb && 'canvas-blur-setup', thumbClassNames)}
/>
)}
{isProtected && <span className="protector" />}
{shouldRenderSpinner && !shouldRenderDownloadButton && (

View File

@ -260,7 +260,7 @@ const RoundVideo: FC<OwnProps> = ({
{!shouldRenderSpoiler && (
<canvas
ref={thumbRef}
className={buildClassName('thumbnail', thumbClassNames)}
className={buildClassName('thumbnail', 'canvas-blur-setup', thumbClassNames)}
style={`width: ${ROUND_VIDEO_DIMENSIONS_PX}px; height: ${ROUND_VIDEO_DIMENSIONS_PX}px`}
/>
)}

View File

@ -211,7 +211,9 @@ const Video: FC<OwnProps> = ({
style={style}
onClick={isUploading ? undefined : handleClick}
>
{withBlurredBackground && <canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />}
{withBlurredBackground && (
<canvas ref={blurredBackgroundRef} className="thumbnail canvas-blur-setup blurred-bg" />
)}
{isInline && (
<OptimizedVideo
ref={videoRef}
@ -237,7 +239,7 @@ const Video: FC<OwnProps> = ({
{hasThumb && !isPreviewPreloaded && (
<canvas
ref={thumbRef}
className={buildClassName('thumbnail', thumbClassNames)}
className={buildClassName('thumbnail', !noThumb && 'canvas-blur-setup', thumbClassNames)}
/>
)}
{isProtected && <span className="protector" />}

View File

@ -1,4 +1,3 @@
import type { FC } from '../../../../lib/teact/teact';
import React, { memo } from '../../../../lib/teact/teact';
import type {
@ -22,25 +21,29 @@ import styles from './ReactionButton.module.scss';
const REACTION_SIZE = 1.25 * REM;
const ReactionButton: FC<{
type OwnProps = {
reaction: ApiReactionCount;
containerId: string;
isOwnMessage?: boolean;
recentReactors?: ApiPeer[];
className?: string;
chosenClassName?: string;
shouldDelayInit?: boolean;
observeIntersection?: ObserveFn;
onClick?: (reaction: ApiReaction) => void;
}> = ({
};
const ReactionButton = ({
reaction,
containerId,
isOwnMessage,
recentReactors,
className,
chosenClassName,
shouldDelayInit,
observeIntersection,
onClick,
}) => {
}: OwnProps) => {
const handleClick = useLastCallback(() => {
onClick?.(reaction.reaction);
});
@ -63,6 +66,7 @@ const ReactionButton: FC<{
reaction={reaction.reaction}
size={REACTION_SIZE}
observeIntersection={observeIntersection}
shouldDelayInit={shouldDelayInit}
/>
{recentReactors?.length ? (
<AvatarList size="mini" peers={recentReactors} />

View File

@ -10,12 +10,14 @@ import type {
ApiSavedReactionTag,
} from '../../../../api/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
import type { Signal } from '../../../../util/signals';
import { getReactionKey, isReactionChosen } from '../../../../global/helpers';
import { selectPeer } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { getMessageKey } from '../../../../util/messageKey';
import useDerivedState from '../../../../hooks/useDerivedState';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
@ -33,6 +35,7 @@ type OwnProps = {
isCurrentUserPremium?: boolean;
observeIntersection?: ObserveFn;
noRecentReactors?: boolean;
getIsMessageListReady: Signal<boolean>;
};
const MAX_RECENT_AVATARS = 3;
@ -46,6 +49,7 @@ const Reactions: FC<OwnProps> = ({
noRecentReactors,
isCurrentUserPremium,
tags,
getIsMessageListReady,
}) => {
const {
toggleReaction,
@ -61,6 +65,8 @@ const Reactions: FC<OwnProps> = ({
results.reduce((acc, reaction) => acc + reaction.count, 0)
), [results]);
const isMessageListReady = useDerivedState(getIsMessageListReady);
const recentReactorsByReactionKey = useMemo(() => {
const global = getGlobal();
@ -149,6 +155,7 @@ const Reactions: FC<OwnProps> = ({
onClick={handleClick}
onRemove={handleRemoveReaction}
observeIntersection={observeIntersection}
shouldDelayInit={!isMessageListReady}
/>
) : (
<ReactionButton
@ -161,6 +168,7 @@ const Reactions: FC<OwnProps> = ({
reaction={reaction}
onClick={handleClick}
observeIntersection={observeIntersection}
shouldDelayInit={!isMessageListReady}
/>
)
))}

View File

@ -1,4 +1,3 @@
import type { FC } from '../../../../lib/teact/teact';
import React, { memo, useRef } from '../../../../lib/teact/teact';
import { getActions } from '../../../../global';
@ -28,7 +27,7 @@ const REACTION_SIZE = 1.25 * REM;
const TITLE_MAX_LENGTH = 15;
const LOOP_LIMIT = 1;
const SavedTagButton: FC<{
type OwnProps = {
reaction: ApiReaction;
tag?: ApiSavedReactionTag;
containerId: string;
@ -39,10 +38,13 @@ const SavedTagButton: FC<{
chosenClassName?: string;
isDisabled?: boolean;
withContextMenu?: boolean;
shouldDelayInit?: boolean;
observeIntersection?: ObserveFn;
onClick?: (reaction: ApiReaction) => void;
onRemove?: (reaction: ApiReaction) => void;
}> = ({
};
const SavedTagButton = ({
reaction,
tag,
containerId,
@ -53,10 +55,11 @@ const SavedTagButton: FC<{
withCount,
isDisabled,
withContextMenu,
shouldDelayInit,
observeIntersection,
onClick,
onRemove,
}) => {
}: OwnProps) => {
const { editSavedReactionTag } = getActions();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLButtonElement>(null);
@ -138,6 +141,7 @@ const SavedTagButton: FC<{
loopLimit={LOOP_LIMIT}
size={REACTION_SIZE}
observeIntersection={observeIntersection}
shouldDelayInit={shouldDelayInit}
/>
{hasText && (
<span className={styles.tagText}>

View File

@ -52,6 +52,8 @@ export default function useCanvasBlur(
ctx.drawImage(img, -radius * 2, -radius * 2, width + radius * 4, height + radius * 4);
canvas.classList.remove('canvas-blur-setup');
if (!IS_CANVAS_FILTER_SUPPORTED) {
fastBlur(ctx, 0, 0, width, height, radius, ITERATIONS);
}

View File

@ -8,9 +8,9 @@
-webkit-user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
&.lovely-chart--state-invisible > * {
display: none;
}
// &.lovely-chart--state-invisible > * {
// display: none;
// }
> canvas,
.lovely-chart--tooltip canvas {

View File

@ -201,6 +201,10 @@ body:not(.is-ios) {
}
}
.canvas-blur-setup {
will-change: width, height;
}
.emoji-small {
background: no-repeat;
background-size: var(--emoji-size);