From d8bad50eec0850983993aeb48bcdf502eadebb59 Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 24 Aug 2026 12:40:53 +0200 Subject: [PATCH] perf(app): defer video embed metadata probes until hover (#1893) --- .../components/embeds/media/EmbedVideo.tsx | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/fluxer_app/src/features/channel/components/embeds/media/EmbedVideo.tsx b/fluxer_app/src/features/channel/components/embeds/media/EmbedVideo.tsx index b3db47a42..16b41a3c2 100644 --- a/fluxer_app/src/features/channel/components/embeds/media/EmbedVideo.tsx +++ b/fluxer_app/src/features/channel/components/embeds/media/EmbedVideo.tsx @@ -24,7 +24,7 @@ import {useMediaFavorite} from '@app/features/messaging/hooks/useMediaFavorite'; import {useNearViewport} from '@app/features/messaging/hooks/useNearViewport'; import {createDownloadHandler} from '@app/features/messaging/utils/FileDownloadUtils'; import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils'; -import {buildMediaProxyURL} from '@app/features/messaging/utils/MediaProxyUtils'; +import {buildMediaProxyURL, mediaDevicePixelRatio} from '@app/features/messaging/utils/MediaProxyUtils'; import {attachmentsToViewerItems, findViewerItemIndex} from '@app/features/messaging/utils/MediaViewerItemUtils'; import {decodeThumbHashDataURL} from '@app/features/messaging/utils/ThumbHashUtils'; import {remFromPx} from '@app/features/theme/layout/RemFromPx'; @@ -33,6 +33,7 @@ import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuComma import * as MediaViewerCommands from '@app/features/ui/commands/MediaViewerCommands'; import MobileLayout from '@app/features/ui/state/MobileLayout'; import { + detachMediaElementSource, VideoPlayer, type VideoPlayerMetadata, } from '@app/features/voice/components/media_player/components/VideoPlayer'; @@ -45,7 +46,7 @@ import {PlayIcon, SpeakerHighIcon, SpeakerXIcon} from '@phosphor-icons/react'; import {AnimatePresence, motion} from 'framer-motion'; import {observer} from 'mobx-react-lite'; import type {FC} from 'react'; -import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; const OPEN_VIDEO_DESCRIPTOR = msg({ message: 'Open video', @@ -137,7 +138,8 @@ const MobileVideoOverlay: FC<{ exit={{opacity: 0}} transition={{duration: Accessibility.useReducedMotion ? 0 : 0.2}} src={thumbHashURL} - alt={title ? i18n._(THUMBNAIL_FOR_DESCRIPTOR, {title}) : i18n._(VIDEO_THUMBNAIL_DESCRIPTOR)} + alt="" + aria-hidden={true} className={styles.thumbHashPlaceholder} data-flx="channel.embeds.media.embed-video.mobile-video-overlay.thumbnail-placeholder" /> @@ -213,7 +215,12 @@ const EmbedVideo: FC = observer( const messageViewContext = useMaybeMessageViewContext(); const effectiveSrc = buildMediaProxyURL(src); const isBlob = src.startsWith('blob:'); - const posterSrc = isBlob ? null : buildMediaProxyURL(src, {format: 'webp'}); + const declaredLayout = resolveVideoLayout(getEffectiveVideoLayoutDimensions({width, height}, null), { + maxWidth, + maxHeight, + }); + const posterRequestWidth = Math.max(1, Math.round(declaredLayout.renderDimensions.width * mediaDevicePixelRatio())); + const posterSrc = isBlob ? null : buildMediaProxyURL(src, {format: 'webp', width: posterRequestWidth}); const {ref: visibilityRef, isNearViewport} = useNearViewport({ rememberKey: posterSrc ?? effectiveSrc, }); @@ -231,8 +238,16 @@ const EmbedVideo: FC = observer( const [isPlayingInline, setIsPlayingInline] = useState(false); const [decodedVideoDimensions, setDecodedVideoDimensions] = useState(null); const [posterNaturalDimensions, setPosterNaturalDimensions] = useState(null); + const [metadataProbeArmed, setMetadataProbeArmed] = useState(false); const inlineVideoRef = useRef(null); useInAppMediaSoundCapture(inlineVideoRef); + useLayoutEffect(() => { + if (!isPlayingInline) return; + const inlineVideo = inlineVideoRef.current; + return () => { + detachMediaElementSource(inlineVideo); + }; + }, [isPlayingInline]); const {shouldBlur, gateReason, canReveal, reveal: revealSensitiveMedia} = useMatureMedia(nsfw, channelId); const shouldLoadMedia = isNearViewport && !shouldBlur; const defaultName = @@ -313,6 +328,8 @@ const EmbedVideo: FC = observer( ? !hasDifferentAspectRatio(decodedDimensionsForSrc, posterDimensionsForSrc, 0.05) : true; const effectivePosterSrc = posterMatchesVideo ? posterSrc : null; + const posterKnownLoaded = posterLoaded && posterMatchesVideo; + const presentedPosterSrc = shouldLoadMedia || posterKnownLoaded ? effectivePosterSrc : null; const updateDecodedVideoDimensions = useCallback( (nextWidth: number, nextHeight: number) => { const nextDimensions = normalizeVideoDimensions({width: nextWidth, height: nextHeight}); @@ -481,6 +498,9 @@ const EmbedVideo: FC = observer( const handleInitialPlay = useCallback(() => { setHasPlayed(true); }, []); + const handleMediaPointerEnter = useCallback(() => { + setMetadataProbeArmed(true); + }, []); const containerStyles: React.CSSProperties = isMobile ? { aspectRatio, @@ -514,6 +534,7 @@ const EmbedVideo: FC = observer( = observer( {disableDelete: !!isPreview || snapshotIndex !== undefined}, ); const metadataProbe = - shouldLoadMedia && !fillContainer && !isBlob && !hasPlayed && !isPlayingInline ? ( + metadataProbeArmed && shouldLoadMedia && !fillContainer && !isBlob && !hasPlayed && !isPlayingInline ? (