From b77dff03c1476baa07827146d4eb17c34d13795d Mon Sep 17 00:00:00 2001 From: JSC Date: Sun, 21 Sep 2025 20:32:29 +0200 Subject: [PATCH] feat: optimize player state updates and memoize calculations to prevent unnecessary re-renders --- src/components/player/Player.tsx | 87 +++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx index 8d1ceb2..2cbd9c1 100644 --- a/src/components/player/Player.tsx +++ b/src/components/player/Player.tsx @@ -40,13 +40,58 @@ import { Volume2, VolumeX, } from 'lucide-react' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { Playlist } from './Playlist' import { NumberFlowDuration } from '../ui/number-flow-duration' export type PlayerDisplayMode = 'normal' | 'minimized' | 'maximized' | 'sidebar' +// Helper function to deep compare player states to prevent unnecessary re-renders +function isPlayerStateEqual(state1: PlayerState, state2: PlayerState): boolean { + // Quick reference equality check first + if (state1 === state2) return true + + // Compare primitive properties + if ( + state1.status !== state2.status || + state1.mode !== state2.mode || + state1.volume !== state2.volume || + state1.previous_volume !== state2.previous_volume || + state1.position !== state2.position || + state1.duration !== state2.duration || + state1.index !== state2.index + ) { + return false + } + + // Compare current_sound object + if (state1.current_sound !== state2.current_sound) { + if (!state1.current_sound || !state2.current_sound) return false + if ( + state1.current_sound.id !== state2.current_sound.id || + state1.current_sound.name !== state2.current_sound.name || + state1.current_sound.thumbnail !== state2.current_sound.thumbnail || + state1.current_sound.extract_url !== state2.current_sound.extract_url + ) { + return false + } + } + + // Compare playlist object (only shallow comparison for performance) + if (state1.playlist !== state2.playlist) { + if (!state1.playlist || !state2.playlist) return false + if ( + state1.playlist.id !== state2.playlist.id || + state1.playlist.sounds.length !== state2.playlist.sounds.length + ) { + return false + } + } + + return true +} + interface PlayerProps { className?: string onPlayerModeChange?: (mode: PlayerDisplayMode) => void @@ -98,11 +143,18 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) { loadState() }, []) - // Listen for player state updates + // Listen for player state updates with optimization + const stateRef = useRef(state) + stateRef.current = state + useEffect(() => { const handlePlayerState = (...args: unknown[]) => { const newState = args[0] as PlayerState - setState(newState) + + // Only update state if it actually changed to prevent unnecessary re-renders + if (!isPlayerStateEqual(stateRef.current, newState)) { + setState(newState) + } } playerEvents.on(PLAYER_EVENTS.PLAYER_STATE, handlePlayerState) @@ -227,7 +279,8 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) { } }, []) - const getModeIcon = () => { + // Memoize expensive calculations to prevent unnecessary re-computations + const modeIcon = useMemo(() => { switch (state.mode) { case 'continuous': return @@ -240,7 +293,17 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) { default: return } - } + }, [state.mode]) + + const progressPercentage = useMemo(() => + (state.position / (state.duration || 1)) * 100, + [state.position, state.duration] + ) + + const modeLabel = useMemo(() => + state.mode.replace('_', ' '), + [state.mode] + ) const expandFromSidebar = useCallback(() => { setDisplayMode('normal') @@ -411,7 +474,7 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) { {/* Progress Bar */}
{ const rect = e.currentTarget.getBoundingClientRect() @@ -434,9 +497,9 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) { variant="ghost" onClick={handleModeChange} className="h-8 w-8 p-0" - title={`Mode: ${state.mode.replace('_', ' ')}`} + title={`Mode: ${modeLabel}`} > - {getModeIcon()} + {modeIcon} - {state.mode.replace('_', ' ')} + {modeLabel}