perf(app): cut idle renderer CPU and unbounded memory growth (#1724)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-18 23:13:42 +02:00
1 parent b4a5eb77a8
commit 5660972c71
13 files changed
+453 -345

No files matched your search

@@ -21,6 +21,7 @@ import {
reduceGIFVRenderState,
} from '@app/features/channel/components/embeds/media/GIFVRenderStateMachine';
import {GifIndicator} from '@app/features/channel/components/embeds/media/GifIndicator';
import {useGifViewportGate} from '@app/features/channel/components/embeds/media/GifViewportGate';
import {getMediaButtonVisibility} from '@app/features/channel/components/embeds/media/MediaButtonUtils';
import {MediaContainer, shouldShowOverlays} from '@app/features/channel/components/embeds/media/MediaContainer';
import type {BaseMediaProps} from '@app/features/channel/components/embeds/media/MediaTypes';
@@ -985,9 +986,15 @@ export const EmbedGif: FC<
const imgRef = useRef<HTMLImageElement>(null);
const animatedImageRef = useRef<HTMLImageElement>(null);
const freezeCanvasRef = useRef<HTMLCanvasElement>(null);
const {ref: visibilityRef, isNearViewport} = useNearViewport<HTMLDivElement>({
disabled: !isMobile,
const {shouldBlur, gateReason, canReveal, reveal: revealSensitiveMedia} = useMatureMedia(nsfw, channelId);
const {
ref: visibilityRef,
loadMedia: shouldLoadMedia,
animate: shouldAnimate,
} = useGifViewportGate<HTMLDivElement>({
element: containerElement,
rememberKey: proxyURL,
shouldBlur,
});
const setContainerRef = useCallback(
(node: HTMLDivElement | null) => {
@@ -1018,8 +1025,6 @@ export const EmbedGif: FC<
Math.round(displayHeight * 2),
);
const animatedImageSourceRef = useRef(optimizedAnimatedURL);
const {shouldBlur, gateReason, canReveal, reveal: revealSensitiveMedia} = useMatureMedia(nsfw, channelId);
const shouldLoadMedia = isNearViewport && !shouldBlur;
const {
loaded,
error,
@@ -1049,7 +1054,7 @@ export const EmbedGif: FC<
status: ImageCacheUtils.hasImage(optimizedAnimatedURL) ? 'ready' : 'idle',
});
const [decoderRequested, setDecoderRequested] = useState(() => getAnimatedMediaPlaybackAllowed());
const shouldUseDecoder = shouldLoadMedia && hasStartedAnimating;
const shouldUseDecoder = shouldAnimate && hasStartedAnimating;
const decoderPlaying =
shouldUseDecoder && decoderRequested && animatedMediaPlaybackAllowed && (gifAutoPlay || isHoveredState);
const decoderState = useAnimatedImageDecoder({
@@ -1061,7 +1066,7 @@ export const EmbedGif: FC<
const useDecoder =
shouldUseDecoder && decoderRequested && decoderState.supported && hasStartedAnimating && !decoderState.error;
const decoderCanvasVisible = useDecoder && decoderState.loaded;
const shouldRenderFreezeFrame = !decoderCanvasVisible && (gifAutoPlay || hasStartedAnimating);
const shouldRenderFreezeFrame = shouldAnimate && !decoderCanvasVisible && (gifAutoPlay || hasStartedAnimating);
const setAnimatedImageRef = useCallback(
(node: HTMLImageElement | null) => {
animatedImageRef.current = node;
@@ -1406,7 +1411,7 @@ export const EmbedGif: FC<
...(aspectRatio ? {aspectRatio} : {}),
} as React.CSSProperties;
const shouldUseAnimatedImage =
shouldLoadMedia &&
shouldAnimate &&
animatedMediaPlaybackAllowed &&
!decoderCanvasVisible &&
(gifAutoPlay || (hasStartedAnimating && isHoveredState));
@@ -356,9 +356,9 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
() => {
if (!active) return;
setLoadedPosterSrc(posterSrc);
const image = ImageCacheUtils.getImage(posterSrc);
if (active && image && image.naturalWidth > 0 && image.naturalHeight > 0) {
setPosterNaturalDimensions({src: posterSrc, width: image.naturalWidth, height: image.naturalHeight});
const posterSize = ImageCacheUtils.getImageSize(posterSrc);
if (active && posterSize) {
setPosterNaturalDimensions({src: posterSrc, width: posterSize.width, height: posterSize.height});
}
},
() => {
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {useNearViewport} from '@app/features/messaging/hooks/useNearViewport';
import {observeIntersection} from '@app/features/platform/utils/SharedIntersectionObserver';
import {useEffect, useState} from 'react';
const ANIMATION_ROOT_MARGIN = '600px 0px';
export interface GifViewportGate {
loadMedia: boolean;
animate: boolean;
}
export function resolveGifViewportGate({
isNearViewport,
isInViewport,
shouldBlur,
}: {
isNearViewport: boolean;
isInViewport: boolean;
shouldBlur: boolean;
}): GifViewportGate {
if (shouldBlur) return {loadMedia: false, animate: false};
return {loadMedia: isNearViewport, animate: isNearViewport && isInViewport};
}
export function useGifViewportGate<T extends Element>({
element,
rememberKey,
shouldBlur,
}: {
element: Element | null;
rememberKey: string;
shouldBlur: boolean;
}): GifViewportGate & {ref: (node: T | null) => void} {
const {ref, isNearViewport} = useNearViewport<T>({rememberKey});
const [isInViewport, setIsInViewport] = useState(() => typeof IntersectionObserver === 'undefined');
useEffect(() => {
if (typeof IntersectionObserver === 'undefined') {
setIsInViewport(true);
return;
}
if (element == null) {
setIsInViewport(false);
return;
}
return observeIntersection(
element,
(entry) => {
setIsInViewport(entry.isIntersecting || entry.intersectionRatio > 0);
},
{rootMargin: ANIMATION_ROOT_MARGIN},
);
}, [element]);
return {ref, ...resolveGifViewportGate({isNearViewport, isInViewport, shouldBlur})};
}