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