From 8e3231466787684118244a799f48cd4d3dd30952 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Wed, 31 Dec 2025 10:39:26 +0100 Subject: [PATCH] refactor: improve performance in rundown - leverage compiler - correct subscriptions from zustand --- .../src/common/hooks-query/useRundown.ts | 4 +-- apps/client/src/common/hooks/useSocket.ts | 36 +++++-------------- .../panel/network-panel/NetworkLogPanel.tsx | 2 +- .../client/src/features/operator/Operator.tsx | 2 +- .../src/features/overview/OverviewWrapper.tsx | 3 +- .../overview/composite/TimeElements.tsx | 6 ++-- .../src/features/rundown/RundownEntry.tsx | 2 ++ .../entry-editor/RundownEntryEditor.tsx | 19 ++++------ .../rundown/rundown-delay/RundownDelay.tsx | 2 ++ .../rundown/rundown-event/RundownEvent.tsx | 16 +++++++-- .../composite/RundownEventChip.tsx | 2 +- .../rundown/rundown-group/RundownGroup.tsx | 6 +++- .../rundown/rundown-header/RundownHeader.tsx | 2 +- .../rundown-header/RundownHeaderMobile.tsx | 2 +- .../rundown-milestone/RundownMilestone.tsx | 6 +++- apps/client/src/views/backstage/Backstage.tsx | 2 +- apps/client/src/views/countdown/Countdown.tsx | 2 +- .../countdown/CountdownSubscriptions.tsx | 4 +-- .../cuesheet/cuesheet-table/CuesheetTable.tsx | 2 +- .../src/views/timeline/TimelinePage.tsx | 4 +-- 20 files changed, 61 insertions(+), 63 deletions(-) diff --git a/apps/client/src/common/hooks-query/useRundown.ts b/apps/client/src/common/hooks-query/useRundown.ts index ecbb2fb99..b97d55161 100644 --- a/apps/client/src/common/hooks-query/useRundown.ts +++ b/apps/client/src/common/hooks-query/useRundown.ts @@ -33,7 +33,7 @@ export default function useRundown() { export function useRundownWithMetadata() { const { data, status } = useRundown(); - const { selectedEventId } = useSelectedEventId(); + const selectedEventId = useSelectedEventId(); const rundownMetadata = useMemo(() => getRundownMetadata(data, selectedEventId), [data, selectedEventId]); return { data, status, rundownMetadata }; } @@ -57,7 +57,7 @@ export function useFlatRundown() { export function useFlatRundownWithMetadata() { const { data, status } = useRundown(); - const { selectedEventId } = useSelectedEventId(); + const selectedEventId = useSelectedEventId(); const rundownWithMetadata = useMemo(() => getFlatRundownMetadata(data, selectedEventId), [data, selectedEventId]); return { data: rundownWithMetadata, status }; diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index cc10945d4..206916b57 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -128,13 +128,9 @@ export const setAuxTimer = { setDuration: (index: number, time: number) => sendSocket('auxtimer', { [index]: { duration: time } }), }; -export const useSelectedEventId = createSelector((state: RuntimeStore) => ({ - selectedEventId: state.eventNow?.id ?? null, -})); +export const useSelectedEventId = createSelector((state: RuntimeStore) => state.eventNow?.id ?? null); -export const useCurrentGroupId = createSelector((state: RuntimeStore) => ({ - currentGroupId: state.groupNow?.id ?? null, -})); +export const useCurrentGroupId = createSelector((state: RuntimeStore) => state.groupNow?.id ?? null); export const setEventPlayback = { loadEvent: (id: string) => sendSocket('load', { id }), @@ -147,9 +143,7 @@ export const useTimer = createSelector((state: RuntimeStore) => ({ ...state.timer, })); -export const useClock = createSelector((state: RuntimeStore) => ({ - clock: state.clock, -})); +export const useClock = createSelector((state: RuntimeStore) => state.clock); export const useNextFlag = createSelector((state: RuntimeStore) => ({ id: state.eventFlag?.id ?? null, @@ -173,28 +167,16 @@ export const useExpectedStartData = createSelector((state: RuntimeStore) => ({ clock: state.clock, })); -export const usePing = createSelector((state: RuntimeStore) => ({ - ping: state.ping, -})); +export const usePing = createSelector((state: RuntimeStore) => state.ping); /** convert ping into a derived value which changes less often */ -export const useIsOnline = createSelector((state: RuntimeStore) => ({ - isOnline: state.ping > 0, -})); +export const useIsOnline = createSelector((state: RuntimeStore) => state.ping > 0); -export const useOffsetMode = createSelector((state: RuntimeStore) => ({ - offsetMode: state.offset.mode, -})); +export const useOffsetMode = createSelector((state: RuntimeStore) => state.offset.mode); export const setOffsetMode = (payload: OffsetMode) => sendSocket('offsetmode', payload); -export const usePlayback = () => { - const featureSelector = (state: RuntimeStore) => ({ - playback: state.timer.playback, - }); - - return useRuntimeStore(featureSelector); -}; +export const usePlayback = createSelector((state: RuntimeStore) => state.timer.playback); /* ======================= Overview data subscriptions ======================= */ @@ -204,9 +186,7 @@ export const useStartTimesOverview = createSelector((state: RuntimeStore) => ({ plannedEnd: state.rundown.plannedEnd, })); -export const useRundownExpectedEnd = createSelector((state: RuntimeStore) => ({ - expectedEnd: state.offset.expectedRundownEnd, -})); +export const useRundownExpectedEnd = createSelector((state: RuntimeStore) => state.offset.expectedRundownEnd); export const useProgressOverview = createSelector((state: RuntimeStore) => ({ numEvents: state.rundown.numEvents, diff --git a/apps/client/src/features/app-settings/panel/network-panel/NetworkLogPanel.tsx b/apps/client/src/features/app-settings/panel/network-panel/NetworkLogPanel.tsx index 075cb9a0e..6d6ed9a6f 100644 --- a/apps/client/src/features/app-settings/panel/network-panel/NetworkLogPanel.tsx +++ b/apps/client/src/features/app-settings/panel/network-panel/NetworkLogPanel.tsx @@ -34,7 +34,7 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) { } function OntimeCloudStats() { - const { ping } = usePing(); + const ping = usePing(); /** * Send immediate ping request, and keep sending on an interval diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index 9d953c8af..43e1aaabb 100644 --- a/apps/client/src/features/operator/Operator.tsx +++ b/apps/client/src/features/operator/Operator.tsx @@ -43,7 +43,7 @@ export default function OperatorLoader() { } function Operator({ rundown, rundownMetadata, customFields, settings }: OperatorData) { - const { selectedEventId } = useSelectedEventId(); + const selectedEventId = useSelectedEventId(); const { subscribe, mainSource, secondarySource, shouldEdit, hidePast, showStart } = useOperatorOptions(); const [showEditPrompt, setShowEditPrompt] = useState(false); diff --git a/apps/client/src/features/overview/OverviewWrapper.tsx b/apps/client/src/features/overview/OverviewWrapper.tsx index f78b59474..9ea23ef8b 100644 --- a/apps/client/src/features/overview/OverviewWrapper.tsx +++ b/apps/client/src/features/overview/OverviewWrapper.tsx @@ -11,7 +11,8 @@ interface OverviewWrapperProps { } export function OverviewWrapper({ navElements, children }: PropsWithChildren) { - const { isOnline } = useIsOnline(); + const isOnline = useIsOnline(); + return (
diff --git a/apps/client/src/features/overview/composite/TimeElements.tsx b/apps/client/src/features/overview/composite/TimeElements.tsx index 97948881a..7c1633b31 100644 --- a/apps/client/src/features/overview/composite/TimeElements.tsx +++ b/apps/client/src/features/overview/composite/TimeElements.tsx @@ -127,7 +127,7 @@ export function StartTimes({ shouldFormat }: OverviewTimeElementsProps) { * Extracted to improve performance as this is a ticking value */ function RundownExpectedEnd({ shouldFormat }: OverviewTimeElementsProps) { - const { expectedEnd } = useRundownExpectedEnd(); + const expectedEnd = useRundownExpectedEnd(); const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]); const maybeExpectedEndText = (() => { @@ -169,7 +169,7 @@ export function MetadataTimes() { function GroupTimes() { const { clock, mode, groupExpectedEnd, actualGroupStart, currentDay, playback } = useGroupTimerOverView(); - const { currentGroupId } = useCurrentGroupId(); + const currentGroupId = useCurrentGroupId(); const group = useEntry(currentGroupId) as OntimeGroup | null; const active = isPlaybackActive(playback); @@ -298,7 +298,7 @@ export function OffsetOverview() { } export function ClockOverview({ shouldFormat, className }: OverviewTimeElementsProps & { className?: string }) { - const { clock } = useClock(); + const clock = useClock(); const formattedClock = shouldFormat ? formatTime(clock) : millisToString(clock); return ( diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index 950918f74..7a180a302 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -32,6 +32,8 @@ export default function RundownEntry({ totalGap, isLinkedToLoaded, }: RundownEntryProps) { + 'use memo'; + if (isOntimeEvent(data)) { return ( state.selectedEvents); const { data } = useRundown(); - const [entry, setEntry] = useState(null); - - useEffect(() => { + const entry = useMemo(() => { if (data.order.length === 0) { - setEntry(null); - return; + return null; } const selectedEventId = Array.from(selectedEvents).at(0); if (!selectedEventId) { - setEntry(null); - return; + return null; } const event = data.entries[selectedEventId]; if (event && !isOntimeDelay(event)) { - setEntry(event); - } else { - setEntry(null); + return event; } - }, [data.order, data.entries, selectedEvents]); + return null; + }, [data.order.length, data.entries, selectedEvents]); if (!entry) { return ; diff --git a/apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx b/apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx index b3ae36237..2f0f32cc0 100644 --- a/apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx +++ b/apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx @@ -17,6 +17,8 @@ interface RundownDelayProps { } export default function RundownDelay({ data, hasCursor }: RundownDelayProps) { + 'use memo'; + const { applyDelay, deleteEntry } = useEntryActions(); const handleRef = useRef(null); diff --git a/apps/client/src/features/rundown/rundown-event/RundownEvent.tsx b/apps/client/src/features/rundown/rundown-event/RundownEvent.tsx index 70215cffa..688ef73b4 100644 --- a/apps/client/src/features/rundown/rundown-event/RundownEvent.tsx +++ b/apps/client/src/features/rundown/rundown-event/RundownEvent.tsx @@ -91,10 +91,21 @@ export default function RundownEvent({ isLinkedToLoaded, hasTriggers, }: RundownEventProps) { - const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping(); + 'use memo'; + + const selectedEventId = useEventIdSwapping((state) => state.selectedEventId); + const setSelectedEventId = useEventIdSwapping((state) => state.setSelectedEventId); + const clearSelectedEventId = useEventIdSwapping((state) => state.clearSelectedEventId); + const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useEntryActions(); - const { selectedEvents, unselect, setSelectedEvents, clearSelectedEvents } = useEventSelection(); + const isSelected = useEventSelection((state) => state.selectedEvents.has(eventId)); + const unselect = useEventSelection((state) => state.unselect); + const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents); + const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents); + + const selectedEvents = useEventSelection((state) => state.selectedEvents); + const handleRef = useRef(null); const [isVisible, setIsVisible] = useState(false); @@ -250,7 +261,6 @@ export default function RundownEvent({ }; }, [handleRef]); - const isSelected = selectedEvents.has(eventId); const blockClasses = cx([ style.rundownEvent, skip ? style.skip : null, diff --git a/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx b/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx index 9a99799bb..944a25473 100644 --- a/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx +++ b/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx @@ -35,7 +35,7 @@ export default function RundownEventChip({ duration, isLinkedToLoaded, }: RundownEventChipProps) { - const { playback } = usePlayback(); + const playback = usePlayback(); if (isLoaded) { return null; diff --git a/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx b/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx index fab8562c9..e8d63252c 100644 --- a/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx +++ b/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx @@ -33,9 +33,13 @@ interface RundownGroupProps { //TODO: the group should maybe include a multiple day indicator export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }: RundownGroupProps) { + 'use memo'; + const handleRef = useRef(null); const { clone, ungroup, deleteEntry } = useEntryActions(); - const { selectedEvents, setSingleEntrySelection } = useEventSelection(); + + const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection); + const selectedEvents = useEventSelection((state) => state.selectedEvents); const [onContextMenu] = useContextMenu([ { diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx index e9fce4f0d..f04325f33 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx +++ b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx @@ -24,7 +24,7 @@ export default memo(RundownHeader); function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) { const [editorMode, setEditorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit }); - const { offsetMode } = useOffsetMode(); + const offsetMode = useOffsetMode(); const toggleAppMode = (mode: AppMode[]) => { // we need to stop user from deselecting a mode diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeaderMobile.tsx b/apps/client/src/features/rundown/rundown-header/RundownHeaderMobile.tsx index f91952f1d..7f8457f39 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeaderMobile.tsx +++ b/apps/client/src/features/rundown/rundown-header/RundownHeaderMobile.tsx @@ -18,7 +18,7 @@ function RundownHeader() { defaultValue: AppMode.Edit, }); - const { offsetMode } = useOffsetMode(); + const offsetMode = useOffsetMode(); const toggleAppMode = (mode: AppMode[]) => { // we need to stop user from deselecting a mode diff --git a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx index 0cc21f58f..e4497f459 100644 --- a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx +++ b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx @@ -22,9 +22,13 @@ interface RundownMilestoneProps { } export default function RundownMilestone({ colour, cue, entryId, hasCursor, title }: RundownMilestoneProps) { + 'use memo'; + const handleRef = useRef(null); const { updateEntry, deleteEntry } = useEntryActions(); - const { selectedEvents, setSingleEntrySelection } = useEventSelection(); + + const selectedEvents = useEventSelection((state) => state.selectedEvents); + const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection); const [onContextMenu] = useContextMenu([ { diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index 5f79c0a9c..35ffaacc3 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -215,7 +215,7 @@ function ExtraInfo({ projectData, size, source }: ExtraInfoProps) { function BackstageClock() { const { getLocalizedString } = useTranslation(); - const { clock } = useClock(); + const clock = useClock(); // gather timer data const formattedClock = formatTime(clock); diff --git a/apps/client/src/views/countdown/Countdown.tsx b/apps/client/src/views/countdown/Countdown.tsx index 4d523d7c2..17192729b 100644 --- a/apps/client/src/views/countdown/Countdown.tsx +++ b/apps/client/src/views/countdown/Countdown.tsx @@ -141,7 +141,7 @@ function CountdownContents({ playableEvents, subscriptions, goToEditMode }: Coun function CountdownClock() { const { getLocalizedString } = useTranslation(); - const { clock } = useClock(); + const clock = useClock(); // gather timer data const formattedClock = formatTime(clock); diff --git a/apps/client/src/views/countdown/CountdownSubscriptions.tsx b/apps/client/src/views/countdown/CountdownSubscriptions.tsx index e4a59d354..724d821e8 100644 --- a/apps/client/src/views/countdown/CountdownSubscriptions.tsx +++ b/apps/client/src/views/countdown/CountdownSubscriptions.tsx @@ -37,8 +37,8 @@ interface CountdownSubscriptionsProps { export default function CountdownSubscriptions({ subscribedEvents, goToEditMode }: CountdownSubscriptionsProps) { const { mainSource, secondarySource, showExpected } = useCountdownOptions(); - const { playback } = usePlayback(); - const { selectedEventId } = useSelectedEventId(); + const playback = usePlayback(); + const selectedEventId = useSelectedEventId(); const showFab = useFadeOutOnInactivity(true); const { data: reportData } = useReport(); diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx index da4804895..a63392fe4 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx @@ -42,7 +42,7 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues const hideTableSeconds = useOptions((state) => state.hideTableSeconds); const hideIndexColumn = useOptions((state) => state.hideIndexColumn); - const { selectedEventId } = useSelectedEventId(); + const selectedEventId = useSelectedEventId(); const virtuosoRef = useRef(null); const { listeners } = useTableNav(); diff --git a/apps/client/src/views/timeline/TimelinePage.tsx b/apps/client/src/views/timeline/TimelinePage.tsx index 92dd08198..6d2677c74 100644 --- a/apps/client/src/views/timeline/TimelinePage.tsx +++ b/apps/client/src/views/timeline/TimelinePage.tsx @@ -36,7 +36,7 @@ export default function TimelinePageLoader() { } function TimelinePage({ events, customFields, projectData, settings }: TimelineData) { - const { selectedEventId } = useSelectedEventId(); + const selectedEventId = useSelectedEventId(); const { mainSource } = useTimelineOptions(); // holds copy of the rundown with only relevant events const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId); @@ -73,7 +73,7 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD function TimelineClock() { const { getLocalizedString } = useTranslation(); - const { clock } = useClock(); + const clock = useClock(); // gather timer data const formattedClock = formatTime(clock);