feat: optimize player state updates and memoize calculations to prevent unnecessary re-renders
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped

This commit is contained in:
JSC
2025-09-21 20:32:29 +02:00
parent 8945eb6ad6
commit b77dff03c1

View File

@@ -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,12 +143,19 @@ 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
// 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 <ArrowRight className="h-4 w-4" />
@@ -240,7 +293,17 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
default:
return <ArrowRightToLine className="h-4 w-4" />
}
}
}, [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 */}
<div className="mb-4">
<Progress
value={(state.position / (state.duration || 1)) * 100}
value={progressPercentage}
className="w-full h-2 cursor-pointer"
onClick={e => {
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}
</Button>
<Button
size="sm"
@@ -488,7 +551,7 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
{/* Secondary Controls */}
<div className="flex items-center justify-between">
<Badge variant="secondary" className="text-xs">
{state.mode.replace('_', ' ')}
{modeLabel}
</Badge>
<div className="flex items-center gap-2">
@@ -632,7 +695,7 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
{/* Progress Bar */}
<div className="w-full max-w-md mb-8">
<Progress
value={(state.position / (state.duration || 1)) * 100}
value={progressPercentage}
className="w-full h-3 cursor-pointer"
onClick={e => {
const rect = e.currentTarget.getBoundingClientRect()
@@ -694,9 +757,9 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={handleModeChange}>
{getModeIcon()}
{modeIcon}
</Button>
<Badge variant="secondary">{state.mode.replace('_', ' ')}</Badge>
<Badge variant="secondary">{modeLabel}</Badge>
</div>
<div className="flex items-center gap-2">