From e33d856e1060e10eba0f0c40f78b503a8ffe9a47 Mon Sep 17 00:00:00 2001 From: Alan Trebugeais Date: Fri, 8 May 2026 12:33:58 +0200 Subject: [PATCH] useMemo instead of expensive filter operations done everytime --- .../video-editor/timeline/AudioWaveform.tsx | 6 +- .../video-editor/timeline/TimelineEditor.tsx | 12 ++- .../components/overlays/ClipMarkerOverlay.tsx | 6 +- .../viewport/TimelineCanvasRows.tsx | 90 +++++++++++-------- 4 files changed, 71 insertions(+), 43 deletions(-) diff --git a/src/components/video-editor/timeline/AudioWaveform.tsx b/src/components/video-editor/timeline/AudioWaveform.tsx index da6105a1..ebb4696e 100644 --- a/src/components/video-editor/timeline/AudioWaveform.tsx +++ b/src/components/video-editor/timeline/AudioWaveform.tsx @@ -1,5 +1,5 @@ import { useTimelineContext } from "dnd-timeline"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState } from "react"; import type { AudioPeaksData } from "./useAudioPeaks"; interface AudioWaveformProps { @@ -11,7 +11,7 @@ interface AudioWaveformProps { * Automatically syncs with the timeline's visible range so the waveform * scrolls and zooms together with the clip items above it. */ -export default function AudioWaveform({ peaks }: AudioWaveformProps) { +function AudioWaveformComponent({ peaks }: AudioWaveformProps) { const canvasRef = useRef(null); const { range } = useTimelineContext(); const [resizeKey, setResizeKey] = useState(0); @@ -87,3 +87,5 @@ export default function AudioWaveform({ peaks }: AudioWaveformProps) { /> ); } + +export default memo(AudioWaveformComponent); diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index f16a499f..455cde09 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -293,6 +293,12 @@ const TimelineEditor = forwardRef( keyShortcuts, isTimelineFocusedRef, }); + const handleToolbarAddAnnotation = useCallback(() => { + handleAddAnnotation(); + }, [handleAddAnnotation]); + const handleToolbarAddAudio = useCallback(() => { + void handleAddAudio(); + }, [handleAddAudio]); return ( ( onOpenCropEditor={onOpenCropEditor} onAddZoom={handleAddZoom} onSuggestZooms={handleSuggestZooms} - onAddAnnotation={() => handleAddAnnotation()} - onAddAudio={() => { - void handleAddAudio(); - }} + onAddAnnotation={handleToolbarAddAnnotation} + onAddAudio={handleToolbarAddAudio} onSplitClip={handleSplitClip} cropLabel={t("sections.crop", "Crop")} /> diff --git a/src/components/video-editor/timeline/components/overlays/ClipMarkerOverlay.tsx b/src/components/video-editor/timeline/components/overlays/ClipMarkerOverlay.tsx index 4dd66c8d..14427795 100644 --- a/src/components/video-editor/timeline/components/overlays/ClipMarkerOverlay.tsx +++ b/src/components/video-editor/timeline/components/overlays/ClipMarkerOverlay.tsx @@ -1,12 +1,12 @@ import { useTimelineContext } from "dnd-timeline"; -import { useMemo } from "react"; +import { memo, useMemo } from "react"; import { calculateAxisScale } from "../../core/time"; interface ClipMarkerOverlayProps { videoDurationMs: number; } -export default function ClipMarkerOverlay({ videoDurationMs }: ClipMarkerOverlayProps) { +function ClipMarkerOverlayComponent({ videoDurationMs }: ClipMarkerOverlayProps) { const { direction, range, valueToPixels } = useTimelineContext(); const sideProperty = direction === "rtl" ? "right" : "left"; @@ -51,3 +51,5 @@ export default function ClipMarkerOverlay({ videoDurationMs }: ClipMarkerOverlay ); } + +export default memo(ClipMarkerOverlayComponent); diff --git a/src/components/video-editor/timeline/components/viewport/TimelineCanvasRows.tsx b/src/components/video-editor/timeline/components/viewport/TimelineCanvasRows.tsx index 01cdf831..ce8ccbb5 100644 --- a/src/components/video-editor/timeline/components/viewport/TimelineCanvasRows.tsx +++ b/src/components/video-editor/timeline/components/viewport/TimelineCanvasRows.tsx @@ -1,5 +1,5 @@ import { Plus } from "@phosphor-icons/react"; -import { useMemo, type MouseEventHandler } from "react"; +import { memo, useMemo, type MouseEventHandler } from "react"; import { cn } from "@/lib/utils"; import AudioWaveform from "../../AudioWaveform"; import glassStyles from "../../ItemGlass.module.css"; @@ -46,7 +46,7 @@ interface TimelineCanvasRowsProps { onZoomRowClick: MouseEventHandler; } -export function TimelineCanvasRows({ +function TimelineCanvasRowsComponent({ items, videoDurationMs, selectAllBlocksActive, @@ -69,32 +69,56 @@ export function TimelineCanvasRows({ onZoomRowMouseLeave, onZoomRowClick, }: TimelineCanvasRowsProps) { - const zoomItems = useMemo(() => items.filter((item) => item.rowId === ZOOM_ROW_ID), [items]); - const clipItems = useMemo(() => items.filter((item) => item.rowId === CLIP_ROW_ID), [items]); - const annotationItems = useMemo( - () => items.filter((item) => isAnnotationTrackRowId(item.rowId)), - [items], - ); - const audioItems = useMemo(() => items.filter((item) => isAudioTrackRowId(item.rowId)), [items]); + const { clipItems, zoomItems, annotationRows, audioRows } = useMemo(() => { + const nextClipItems: TimelineRenderItem[] = []; + const nextZoomItems: TimelineRenderItem[] = []; + const annotationBuckets = new Map(); + const audioBuckets = new Map(); - const audioRowIds = useMemo( - () => - Array.from( - new Set(audioItems.map((item) => getAudioTrackRowId(getAudioTrackIndex(item.rowId)))), - ).sort((left, right) => getAudioTrackIndex(left) - getAudioTrackIndex(right)), - [audioItems], - ); - const annotationRowIds = useMemo( - () => - Array.from( - new Set( - annotationItems.map((item) => - getAnnotationTrackRowId(getAnnotationTrackIndex(item.rowId)), - ), - ), - ).sort((left, right) => getAnnotationTrackIndex(left) - getAnnotationTrackIndex(right)), - [annotationItems], - ); + for (const item of items) { + if (item.rowId === CLIP_ROW_ID) { + nextClipItems.push(item); + continue; + } + if (item.rowId === ZOOM_ROW_ID) { + nextZoomItems.push(item); + continue; + } + if (isAnnotationTrackRowId(item.rowId)) { + const trackIndex = getAnnotationTrackIndex(item.rowId); + const bucket = annotationBuckets.get(trackIndex); + if (bucket) bucket.push(item); + else annotationBuckets.set(trackIndex, [item]); + continue; + } + if (isAudioTrackRowId(item.rowId)) { + const trackIndex = getAudioTrackIndex(item.rowId); + const bucket = audioBuckets.get(trackIndex); + if (bucket) bucket.push(item); + else audioBuckets.set(trackIndex, [item]); + } + } + + const annotationRowsSorted = Array.from(annotationBuckets.entries()) + .sort(([left], [right]) => left - right) + .map(([trackIndex, rowItems]) => ({ + rowId: getAnnotationTrackRowId(trackIndex), + items: rowItems, + })); + const audioRowsSorted = Array.from(audioBuckets.entries()) + .sort(([left], [right]) => left - right) + .map(([trackIndex, rowItems]) => ({ + rowId: getAudioTrackRowId(trackIndex), + items: rowItems, + })); + + return { + clipItems: nextClipItems, + zoomItems: nextZoomItems, + annotationRows: annotationRowsSorted, + audioRows: audioRowsSorted, + }; + }, [items]); return ( <> @@ -166,10 +190,7 @@ export function TimelineCanvasRows({ ))} - {annotationRowIds.map((rowId, index) => { - const rowItems = annotationItems.filter( - (item) => getAnnotationTrackRowId(getAnnotationTrackIndex(item.rowId)) === rowId, - ); + {annotationRows.map(({ rowId, items: rowItems }, index) => { return ( { - const rowItems = audioItems.filter( - (item) => getAudioTrackRowId(getAudioTrackIndex(item.rowId)) === rowId, - ); + {audioRows.map(({ rowId, items: rowItems }, index) => { return ( ); } + +export const TimelineCanvasRows = memo(TimelineCanvasRowsComponent);