perf(media): defer image/video decryption until near-viewport (P5-5)

Creates useNearViewport hook (IntersectionObserver, 200px rootMargin,
one-shot disconnect after first trigger). ImageContent and VideoContent
now gate loadSrc() on nearViewport — when autoPlay is enabled, encrypted
media is not decrypted until the element is within 200px of the visible
area, reducing initial page load cost on long timelines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 17:56:34 -04:00
parent a6bf4eb7e7
commit 3df95adc52
4 changed files with 47 additions and 7 deletions
@@ -1,4 +1,4 @@
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import {
Badge,
Box,
@@ -32,6 +32,7 @@ import {
} from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { validBlurHash } from '../../../utils/blurHash';
import { useNearViewport } from '../../../hooks/useNearViewport';
type RenderVideoProps = {
title: string;
@@ -79,6 +80,9 @@ export const VideoContent = as<'div', VideoContentProps>(
const [error, setError] = useState(false);
const [blurred, setBlurred] = useState(markedAsSpoiler ?? false);
const sentinelRef = useRef<HTMLDivElement>(null);
const nearViewport = useNearViewport(sentinelRef);
const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
@@ -106,11 +110,12 @@ export const VideoContent = as<'div', VideoContentProps>(
};
useEffect(() => {
if (autoPlay) loadSrc().catch(() => undefined);
}, [autoPlay, loadSrc]);
if (autoPlay && nearViewport) loadSrc().catch(() => undefined);
}, [autoPlay, nearViewport, loadSrc]);
return (
<Box className={classNames(css.RelativeBase, className)} {...props} ref={ref}>
<div ref={sentinelRef} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }} />
{typeof blurHash === 'string' && !load && (
<BlurhashCanvas
style={{ width: '100%', height: '100%' }}