lazy mount motion preview clips to reduce DOM overhead

This commit is contained in:
Josh Hawkins 2026-03-08 12:13:14 -05:00
parent d1793207b4
commit 297bc5e8a0

View File

@ -624,6 +624,9 @@ export default function MotionPreviewsPane({
const [hasVisibilityData, setHasVisibilityData] = useState(false); const [hasVisibilityData, setHasVisibilityData] = useState(false);
const clipObserver = useRef<IntersectionObserver | null>(null); const clipObserver = useRef<IntersectionObserver | null>(null);
const [mountedClips, setMountedClips] = useState<Set<string>>(new Set());
const mountObserver = useRef<IntersectionObserver | null>(null);
const recordingTimeRange = useMemo(() => { const recordingTimeRange = useMemo(() => {
if (!motionRanges.length) { if (!motionRanges.length) {
return null; return null;
@ -788,15 +791,56 @@ export default function MotionPreviewsPane({
}; };
}, [scrollContainer]); }, [scrollContainer]);
useEffect(() => {
if (!scrollContainer) {
return;
}
const nearClipIds = new Set<string>();
mountObserver.current = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const clipId = (entry.target as HTMLElement).dataset.clipId;
if (!clipId) {
return;
}
if (entry.isIntersecting) {
nearClipIds.add(clipId);
} else {
nearClipIds.delete(clipId);
}
});
setMountedClips(new Set(nearClipIds));
},
{
root: scrollContainer,
rootMargin: "200% 0px",
threshold: 0,
},
);
scrollContainer
.querySelectorAll<HTMLElement>("[data-clip-id]")
.forEach((node) => {
mountObserver.current?.observe(node);
});
return () => {
mountObserver.current?.disconnect();
};
}, [scrollContainer]);
const clipRef = useCallback((node: HTMLElement | null) => { const clipRef = useCallback((node: HTMLElement | null) => {
if (!clipObserver.current) { if (!node) {
return; return;
} }
try { try {
if (node) { clipObserver.current?.observe(node);
clipObserver.current.observe(node); mountObserver.current?.observe(node);
}
} catch { } catch {
// no op // no op
} }
@ -864,31 +908,38 @@ export default function MotionPreviewsPane({
) : ( ) : (
<div className="grid grid-cols-1 gap-2 pb-2 sm:grid-cols-2 md:gap-4 xl:grid-cols-4"> <div className="grid grid-cols-1 gap-2 pb-2 sm:grid-cols-2 md:gap-4 xl:grid-cols-4">
{clipData.map( {clipData.map(
({ range, preview, fallbackFrameTimes, motionHeatmap }, idx) => ( ({ range, preview, fallbackFrameTimes, motionHeatmap }, idx) => {
<div const clipId = `${camera.name}-${range.start_time}-${range.end_time}-${idx}`;
key={`${camera.name}-${range.start_time}-${range.end_time}-${preview?.src ?? "none"}-${idx}`} const isMounted = mountedClips.has(clipId);
data-clip-id={`${camera.name}-${range.start_time}-${range.end_time}-${idx}`}
ref={clipRef} return (
> <div
<MotionPreviewClip key={`${camera.name}-${range.start_time}-${range.end_time}-${preview?.src ?? "none"}-${idx}`}
cameraName={camera.name} data-clip-id={clipId}
range={range} ref={clipRef}
playbackRate={playbackRate} >
preview={preview} {isMounted ? (
fallbackFrameTimes={fallbackFrameTimes} <MotionPreviewClip
motionHeatmap={motionHeatmap} cameraName={camera.name}
nonMotionAlpha={nonMotionAlpha} range={range}
isVisible={ playbackRate={playbackRate}
windowVisible && preview={preview}
(visibleClips.includes( fallbackFrameTimes={fallbackFrameTimes}
`${camera.name}-${range.start_time}-${range.end_time}-${idx}`, motionHeatmap={motionHeatmap}
) || nonMotionAlpha={nonMotionAlpha}
(!hasVisibilityData && idx < 8)) isVisible={
} windowVisible &&
onSeek={onSeek} (visibleClips.includes(clipId) ||
/> (!hasVisibilityData && idx < 8))
</div> }
), onSeek={onSeek}
/>
) : (
<div className="aspect-video rounded-lg bg-black md:rounded-2xl" />
)}
</div>
);
},
)} )}
</div> </div>
)} )}