feat: optimize player state updates and memoize calculations to prevent unnecessary re-renders
This commit is contained in:
@@ -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 <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">
|
||||
|
||||
Reference in New Issue
Block a user