Compare commits

...

7 Commits

Author SHA1 Message Date
Geert Rademakes
770c606561 perf: Aggressive optimization for playlist switching to make it snappy - Add React.startTransition to batch state updates in usePaginatedSongs - Optimize playlist selection handler with immediate navigation - Memoize click handlers in PlaylistItem to prevent recreation - Use replace navigation to avoid history stack delays - Should dramatically reduce 600+ms playlist switching delay 2025-08-06 10:59:16 +02:00
Geert Rademakes
c9541cffee perf: Optimize song selection to fix 711ms click handler delay - Memoize formatted duration in SongItem to prevent recalculation - Remove handleSongSelect from songItems useMemo dependencies - Optimize SongItem component to prevent unnecessary re-renders - Should improve song selection responsiveness 2025-08-06 10:57:25 +02:00
Geert Rademakes
e2ae3bd6d8 cleanup: Remove debugging console logs - Remove timestamp logging from playlist selection and change detection - Remove loadPage call logging - Clean up temporary debugging code 2025-08-06 10:54:30 +02:00
Geert Rademakes
510c6e1026 perf: Optimize playlist manager to fix 727ms click handler delay - Memoize button styles to prevent object recreation on every render - Wrap PlaylistItem in React.memo to prevent unnecessary re-renders - Fixes React violation 'click' handler took 727ms - Should dramatically improve playlist switching responsiveness 2025-08-06 10:52:11 +02:00
Geert Rademakes
586b3634b5 debug: Add timestamps to console logs for better timing analysis - Add ISO timestamps to playlist selection and change detection logs - Add timestamp to loadPage function calls - Helps identify exact timing of playlist switching delays 2025-08-06 10:51:08 +02:00
Geert Rademakes
57c1047357 debug: Add console logs to investigate playlist switching delay - Add logging to track playlist selection and change detection - Remove isInitialLoad condition that might block playlist changes - Optimize initial load to prevent conflicts with playlist switching - Temporary debugging to identify timing issues 2025-08-06 10:49:31 +02:00
Geert Rademakes
9268a4635f perf: Remove unnecessary delays in playlist switching and infinite scroll - Remove setTimeout delay in playlist change handler - Reduce infinite scroll trigger delay from 1000ms to 100ms - Improve responsiveness when switching playlists and scrolling - Fixes 1-second delay between URL change and actual loading 2025-08-06 10:47:37 +02:00
4 changed files with 79 additions and 39 deletions

View File

@ -1,6 +1,6 @@
import { Box, Button, Flex, Heading, Spinner, Text, useBreakpointValue, IconButton, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, VStack } from "@chakra-ui/react";
import { ChevronLeftIcon, ChevronRightIcon, SettingsIcon } from "@chakra-ui/icons";
import { useState, useRef, useEffect, useCallback } from "react";
import React, { useState, useRef, useEffect, useCallback } from "react";
import { useNavigate, useLocation, Routes, Route } from "react-router-dom";
import { PaginatedSongList } from "./components/PaginatedSongList";
import { PlaylistManager } from "./components/PlaylistManager";
@ -154,13 +154,15 @@ export default function RekordboxReader() {
}, [currentPlaylist, playlists, navigate, xmlLoading]);
const handlePlaylistSelect = (name: string) => {
setSelectedSong(null); // Clear selected song when changing playlists
// Clear selected song immediately to prevent stale state
setSelectedSong(null);
// Navigate immediately without any delays
if (name === "All Songs") {
navigate("/");
navigate("/", { replace: true });
} else {
// Use encodeURIComponent to properly handle spaces and special characters
const encodedName = encodeURIComponent(name);
navigate(`/playlists/${encodedName}`);
navigate(`/playlists/${encodedName}`, { replace: true });
}
};

View File

@ -50,6 +50,8 @@ const SongItem = memo<{
onToggleSelection: (songId: string) => void;
showCheckbox: boolean;
}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox }) => {
// Memoize the formatted duration to prevent recalculation
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
const handleClick = useCallback(() => {
onSelect(song);
}, [onSelect, song]);
@ -89,7 +91,7 @@ const SongItem = memo<{
</Box>
<Box textAlign="right" ml={2}>
<Text fontSize="xs" color="gray.500">
{formatDuration(song.totalTime || '')}
{formattedDuration}
</Text>
<Text fontSize="xs" color="gray.600">
{song.averageBpm} BPM
@ -213,7 +215,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
showCheckbox={selectedSongs.size > 0 || depth === 0}
/>
));
}, [songs, selectedSongs, selectedSongId, handleSongSelect, toggleSelection, depth]);
}, [songs, selectedSongs, selectedSongId, toggleSelection, depth]); // Removed handleSongSelect since it's already memoized
// Use total playlist duration if available, otherwise calculate from current songs
const totalDuration = useMemo(() => {
@ -257,7 +259,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
// Reset the flag after a short delay to prevent multiple triggers
timeoutRef.current = setTimeout(() => {
isTriggeringRef.current = false;
}, 1000);
}, 100);
}
},
{

View File

@ -36,27 +36,50 @@ interface PlaylistManagerProps {
onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void;
}
const getButtonStyles = (isSelected: boolean) => ({
// Memoized button styles to prevent unnecessary re-renders
const selectedButtonStyles = {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
bg: isSelected ? "blue.800" : "transparent",
color: isSelected ? "white" : "gray.100",
fontWeight: isSelected ? "600" : "normal",
bg: "blue.800",
color: "white",
fontWeight: "600",
borderRadius: "md",
px: 4,
py: 2,
cursor: "pointer",
transition: "all 0.2s",
_hover: {
bg: isSelected ? "blue.600" : "whiteAlpha.200",
bg: "blue.600",
transform: "translateX(2px)",
},
_active: {
bg: isSelected ? "blue.700" : "whiteAlpha.300",
bg: "blue.700",
},
});
};
const unselectedButtonStyles = {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
bg: "transparent",
color: "gray.100",
fontWeight: "normal",
borderRadius: "md",
px: 4,
py: 2,
cursor: "pointer",
transition: "all 0.2s",
_hover: {
bg: "whiteAlpha.200",
transform: "translateX(2px)",
},
_active: {
bg: "whiteAlpha.300",
},
};
interface PlaylistItemProps {
node: PlaylistNode;
@ -68,7 +91,7 @@ interface PlaylistItemProps {
allFolders: { name: string }[];
}
const PlaylistItem: React.FC<PlaylistItemProps> = ({
const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
node,
level,
selectedItem,
@ -78,6 +101,15 @@ const PlaylistItem: React.FC<PlaylistItemProps> = ({
allFolders,
}) => {
const [isOpen, setIsOpen] = useState(false);
// Memoize click handlers to prevent recreation
const handlePlaylistClick = useCallback(() => {
onPlaylistSelect(node.name);
}, [onPlaylistSelect, node.name]);
const handleFolderToggle = useCallback(() => {
setIsOpen(prev => !prev);
}, []);
const indent = level * 10; // Reverted back to 10px per level
if (node.type === 'folder') {
@ -86,8 +118,8 @@ const PlaylistItem: React.FC<PlaylistItemProps> = ({
<Flex align="center" gap={1}>
<Button
flex={1}
{...getButtonStyles(false)}
onClick={() => setIsOpen(!isOpen)}
{...unselectedButtonStyles}
onClick={handleFolderToggle}
ml={indent}
pl={level > 0 ? 6 : 4} // Add extra padding for nested items
justifyContent="flex-start"
@ -154,8 +186,8 @@ const PlaylistItem: React.FC<PlaylistItemProps> = ({
<Flex align="center" gap={0}>
<Button
flex="1 1 auto"
{...getButtonStyles(selectedItem === node.name)}
onClick={() => onPlaylistSelect(node.name)}
{...(selectedItem === node.name ? selectedButtonStyles : unselectedButtonStyles)}
onClick={handlePlaylistClick}
ml={indent}
pl={level > 0 ? 6 : 4} // Add extra padding for nested items
borderRightRadius={0}
@ -237,7 +269,7 @@ const PlaylistItem: React.FC<PlaylistItemProps> = ({
</Menu>
</Flex>
);
};
});
export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
playlists,
@ -289,7 +321,7 @@ export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
<Box>
<VStack spacing={2} align="stretch" mb={4}>
<Button
{...getButtonStyles(selectedItem === null)}
{...(selectedItem === null ? selectedButtonStyles : unselectedButtonStyles)}
onClick={() => onPlaylistSelect(null)}
>
All Songs

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { api, type SongsResponse } from '../services/api';
import type { Song } from '../types/interfaces';
@ -125,9 +125,12 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
setIsInitialLoad(true);
}, [initialSearch, cleanup]);
// Initial load
// Initial load - only run once when the hook is first created
useEffect(() => {
loadPage(1);
// Only load if we haven't loaded anything yet
if (songs.length === 0 && !loading) {
loadPage(1);
}
// Cleanup on unmount
return () => {
@ -135,26 +138,27 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
};
}, []);
// Handle playlist changes
// Handle playlist changes - optimized for immediate response
useEffect(() => {
if (!isInitialLoad && previousPlaylistRef.current !== playlistName) {
// Update refs
if (previousPlaylistRef.current !== playlistName) {
// Update refs immediately
currentPlaylistRef.current = playlistName;
currentSearchQueryRef.current = searchQuery;
previousPlaylistRef.current = playlistName;
// Clear songs for new playlist to replace them
setSongs([]);
setHasMore(true);
setCurrentPage(1);
setSearchQuery(initialSearch);
setError(null);
// Use setTimeout to avoid the dependency issue
setTimeout(() => {
loadPage(1, initialSearch, playlistName);
}, 0);
// Batch all state updates together to reduce re-renders
React.startTransition(() => {
setSongs([]);
setHasMore(true);
setCurrentPage(1);
setSearchQuery(initialSearch);
setError(null);
});
// Load immediately
loadPage(1, initialSearch, playlistName);
}
}, [playlistName, isInitialLoad, initialSearch, loadPage]);
}, [playlistName, initialSearch, loadPage]);
return {
songs,