useMemo instead of expensive filter operations done everytime

This commit is contained in:
Alan Trebugeais
2026-05-08 12:33:58 +02:00
parent ea945ed62d
commit e33d856e10
4 changed files with 71 additions and 43 deletions
@@ -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<HTMLCanvasElement>(null);
const { range } = useTimelineContext();
const [resizeKey, setResizeKey] = useState(0);
@@ -87,3 +87,5 @@ export default function AudioWaveform({ peaks }: AudioWaveformProps) {
/>
);
}
export default memo(AudioWaveformComponent);
@@ -293,6 +293,12 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
keyShortcuts,
isTimelineFocusedRef,
});
const handleToolbarAddAnnotation = useCallback(() => {
handleAddAnnotation();
}, [handleAddAnnotation]);
const handleToolbarAddAudio = useCallback(() => {
void handleAddAudio();
}, [handleAddAudio]);
return (
<TimelineEditorShell
@@ -313,10 +319,8 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
onOpenCropEditor={onOpenCropEditor}
onAddZoom={handleAddZoom}
onSuggestZooms={handleSuggestZooms}
onAddAnnotation={() => handleAddAnnotation()}
onAddAudio={() => {
void handleAddAudio();
}}
onAddAnnotation={handleToolbarAddAnnotation}
onAddAudio={handleToolbarAddAudio}
onSplitClip={handleSplitClip}
cropLabel={t("sections.crop", "Crop")}
/>
@@ -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
</div>
);
}
export default memo(ClipMarkerOverlayComponent);
@@ -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<HTMLDivElement>;
}
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<number, TimelineRenderItem[]>();
const audioBuckets = new Map<number, TimelineRenderItem[]>();
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({
))}
</Row>
{annotationRowIds.map((rowId, index) => {
const rowItems = annotationItems.filter(
(item) => getAnnotationTrackRowId(getAnnotationTrackIndex(item.rowId)) === rowId,
);
{annotationRows.map(({ rowId, items: rowItems }, index) => {
return (
<Row
key={rowId}
@@ -194,10 +215,7 @@ export function TimelineCanvasRows({
);
})}
{audioRowIds.map((rowId, index) => {
const rowItems = audioItems.filter(
(item) => getAudioTrackRowId(getAudioTrackIndex(item.rowId)) === rowId,
);
{audioRows.map(({ rowId, items: rowItems }, index) => {
return (
<Row
key={rowId}
@@ -224,3 +242,5 @@ export function TimelineCanvasRows({
</>
);
}
export const TimelineCanvasRows = memo(TimelineCanvasRowsComponent);