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, Volume2,
VolumeX, VolumeX,
} from 'lucide-react' } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Playlist } from './Playlist' import { Playlist } from './Playlist'
import { NumberFlowDuration } from '../ui/number-flow-duration' import { NumberFlowDuration } from '../ui/number-flow-duration'
export type PlayerDisplayMode = 'normal' | 'minimized' | 'maximized' | 'sidebar' 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 { interface PlayerProps {
className?: string className?: string
onPlayerModeChange?: (mode: PlayerDisplayMode) => void onPlayerModeChange?: (mode: PlayerDisplayMode) => void
@@ -98,11 +143,18 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
loadState() loadState()
}, []) }, [])
// Listen for player state updates // Listen for player state updates with optimization
const stateRef = useRef(state)
stateRef.current = state
useEffect(() => { useEffect(() => {
const handlePlayerState = (...args: unknown[]) => { const handlePlayerState = (...args: unknown[]) => {
const newState = args[0] as PlayerState 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) 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) { switch (state.mode) {
case 'continuous': case 'continuous':
return <ArrowRight className="h-4 w-4" /> return <ArrowRight className="h-4 w-4" />
@@ -240,7 +293,17 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
default: default:
return <ArrowRightToLine className="h-4 w-4" /> 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(() => { const expandFromSidebar = useCallback(() => {
setDisplayMode('normal') setDisplayMode('normal')
@@ -411,7 +474,7 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
{/* Progress Bar */} {/* Progress Bar */}
<div className="mb-4"> <div className="mb-4">
<Progress <Progress
value={(state.position / (state.duration || 1)) * 100} value={progressPercentage}
className="w-full h-2 cursor-pointer" className="w-full h-2 cursor-pointer"
onClick={e => { onClick={e => {
const rect = e.currentTarget.getBoundingClientRect() const rect = e.currentTarget.getBoundingClientRect()
@@ -434,9 +497,9 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
variant="ghost" variant="ghost"
onClick={handleModeChange} onClick={handleModeChange}
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
title={`Mode: ${state.mode.replace('_', ' ')}`} title={`Mode: ${modeLabel}`}
> >
{getModeIcon()} {modeIcon}
</Button> </Button>
<Button <Button
size="sm" size="sm"
@@ -488,7 +551,7 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
{/* Secondary Controls */} {/* Secondary Controls */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
{state.mode.replace('_', ' ')} {modeLabel}
</Badge> </Badge>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -632,7 +695,7 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
{/* Progress Bar */} {/* Progress Bar */}
<div className="w-full max-w-md mb-8"> <div className="w-full max-w-md mb-8">
<Progress <Progress
value={(state.position / (state.duration || 1)) * 100} value={progressPercentage}
className="w-full h-3 cursor-pointer" className="w-full h-3 cursor-pointer"
onClick={e => { onClick={e => {
const rect = e.currentTarget.getBoundingClientRect() 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-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={handleModeChange}> <Button size="sm" variant="ghost" onClick={handleModeChange}>
{getModeIcon()} {modeIcon}
</Button> </Button>
<Badge variant="secondary">{state.mode.replace('_', ' ')}</Badge> <Badge variant="secondary">{modeLabel}</Badge>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">