# Performance Optimization for Drag-and-Drop ## Table of Contents - [Virtual Scrolling](#virtual-scrolling) - [Throttling and Debouncing](#throttling-and-debouncing) - [CSS Optimization](#css-optimization) - [React Optimization](#react-optimization) - [Large Dataset Handling](#large-dataset-handling) - [Memory Management](#memory-management) ## Virtual Scrolling ### Virtual List with Drag-and-Drop ```tsx import { VariableSizeList } from 'react-window'; import { DndContext, useSortable } from '@dnd-kit/sortable'; interface VirtualDragListProps { items: any[]; itemHeight: number | ((index: number) => number); height: number; onReorder: (items: any[]) => void; } function VirtualDragList({ items, itemHeight, height, onReorder }: VirtualDragListProps) { // Track visible items for drag operations const [visibleRange, setVisibleRange] = useState({ start: 0, end: 10 }); const Row = ({ index, style, data }) => { const item = data[index]; return (
); }; const handleScroll = ({ visibleStartIndex, visibleStopIndex }) => { setVisibleRange({ start: visibleStartIndex, end: visibleStopIndex }); }; return ( {Row} ); } // Optimized draggable item for virtual list const VirtualDraggableItem = memo(({ item, index }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: item.id, disabled: false, }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, // Use transform for positioning, not top/left position: 'relative' as const, }; return (
{item.content}
); }, (prevProps, nextProps) => { // Custom comparison for memoization return prevProps.item.id === nextProps.item.id && prevProps.item.content === nextProps.item.content && prevProps.index === nextProps.index; }); ``` ### Intersection Observer for Lazy Loading ```tsx function LazyLoadDragList({ items, onLoadMore }) { const [visibleItems, setVisibleItems] = useState(items.slice(0, 50)); const loaderRef = useRef(null); useEffect(() => { const observer = new IntersectionObserver( (entries) => { const [entry] = entries; if (entry.isIntersecting && visibleItems.length < items.length) { // Load more items const nextBatch = items.slice( visibleItems.length, visibleItems.length + 50 ); setVisibleItems(prev => [...prev, ...nextBatch]); } }, { root: null, rootMargin: '100px', // Start loading before reaching the end threshold: 0.1, } ); if (loaderRef.current) { observer.observe(loaderRef.current); } return () => observer.disconnect(); }, [items, visibleItems]); return (
{visibleItems.map(item => ( ))} {visibleItems.length < items.length && (
Loading more items...
)}
); } ``` ## Throttling and Debouncing ### Throttled Drag Updates ```tsx function useThrottle any>( callback: T, delay: number ): T { const lastCall = useRef(0); const timeoutRef = useRef(); return useCallback((...args: Parameters) => { const now = Date.now(); if (now - lastCall.current >= delay) { lastCall.current = now; callback(...args); } else { // Schedule for later clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => { lastCall.current = Date.now(); callback(...args); }, delay - (now - lastCall.current)); } }, [callback, delay]) as T; } // Usage in drag handler function OptimizedDragHandler() { const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 }); // Throttle to 60fps (16ms) for smooth animation const throttledUpdatePosition = useThrottle((position) => { setDragPosition(position); // Expensive calculations here }, 16); const handleDragMove = (event) => { const { active } = event; throttledUpdatePosition({ x: active.rect.current.translated.left, y: active.rect.current.translated.top, }); }; return ( {/* Content */} ); } ``` ### Debounced Auto-Save ```tsx function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => { setDebouncedValue(value); }, delay); return () => clearTimeout(timer); }, [value, delay]); return debouncedValue; } // Auto-save after drag operations function AutoSaveDragList({ items, onSave }) { const [localItems, setLocalItems] = useState(items); const debouncedItems = useDebounce(localItems, 500); useEffect(() => { if (debouncedItems !== items) { onSave(debouncedItems); } }, [debouncedItems]); const handleDragEnd = (event) => { // Update local state immediately const newItems = reorderItems(localItems, event); setLocalItems(newItems); // Debounced save will trigger after 500ms of no changes }; return ( {/* Draggable items */} ); } ``` ## CSS Optimization ### High-Performance CSS ```css /* Use transform instead of position for smooth animations */ .draggable-item { transform: translate3d(0, 0, 0); /* Enable GPU acceleration */ will-change: transform; /* Hint browser optimization */ backface-visibility: hidden; /* Prevent flickering */ perspective: 1000px; /* Create stacking context */ } /* Optimize transitions */ .draggable-item-transition { transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1); transition-property: transform; /* Only animate what changes */ } /* Reduce paint areas during drag */ .dragging { pointer-events: none; /* Prevent hover effects */ z-index: 9999; /* Lift above other elements */ position: fixed; /* Take out of document flow */ } /* Optimize shadows (expensive to render) */ .draggable-item { /* Use box-shadow sparingly */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .dragging .draggable-item { /* Larger shadow only when dragging */ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); } /* Use CSS containment */ .drag-container { contain: layout style; /* Isolate layout recalculations */ } /* Optimize hover states */ @media (hover: hover) { .draggable-item:hover { /* Only apply hover on devices that support it */ background-color: var(--hover-color); } } /* Reduce visual complexity during drag */ .drag-container.is-dragging .draggable-item:not(.dragging) { opacity: 0.7; /* Dim non-dragged items */ filter: grayscale(30%); /* Reduce visual noise */ } ``` ### CSS Transform Utilities ```tsx // Utility for efficient transforms export const transformStyles = { translate: (x: number, y: number, scale = 1) => ({ transform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`, WebkitTransform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`, }), rotate: (deg: number) => ({ transform: `rotate(${deg}deg)`, WebkitTransform: `rotate(${deg}deg)`, }), // Combine transforms efficiently combine: (...transforms: string[]) => ({ transform: transforms.join(' '), WebkitTransform: transforms.join(' '), }), }; // Use requestAnimationFrame for smooth animations export function animateTransform( element: HTMLElement, from: { x: number; y: number }, to: { x: number; y: number }, duration: number, onComplete?: () => void ) { const startTime = performance.now(); const animate = (currentTime: number) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); // Easing function const easeProgress = 1 - Math.pow(1 - progress, 3); const x = from.x + (to.x - from.x) * easeProgress; const y = from.y + (to.y - from.y) * easeProgress; element.style.transform = `translate3d(${x}px, ${y}px, 0)`; if (progress < 1) { requestAnimationFrame(animate); } else { onComplete?.(); } }; requestAnimationFrame(animate); } ``` ## React Optimization ### Memoization Strategies ```tsx // Memoize expensive components const MemoizedDraggableItem = memo( ({ item, index, onDragEnd }) => { const { attributes, listeners, setNodeRef, transform, transition, } = useSortable({ id: item.id }); return (
{item.content}
); }, (prevProps, nextProps) => { // Custom equality check return ( prevProps.item.id === nextProps.item.id && prevProps.item.content === nextProps.item.content && prevProps.index === nextProps.index ); } ); // Memoize callbacks function DragListContainer({ items }) { const handleDragEnd = useCallback((event) => { const { active, over } = event; if (active.id !== over.id) { // Reorder logic } }, []); // Empty deps if function doesn't depend on props const sensors = useMemo( () => [ useSensor(PointerSensor), useSensor(KeyboardSensor), ], [] ); return ( {/* Content */} ); } ``` ### State Management Optimization ```tsx // Use local state for drag preview, global for final position function OptimizedDragState({ initialItems }) { const [items, setItems] = useState(initialItems); const [dragPreview, setDragPreview] = useState(null); const [activeId, setActiveId] = useState(null); const handleDragStart = (event) => { setActiveId(event.active.id); // Don't update main state yet setDragPreview({ id: event.active.id, position: { x: 0, y: 0 }, }); }; const handleDragMove = (event) => { // Only update preview position (cheap) setDragPreview(prev => ({ ...prev, position: { x: event.delta.x, y: event.delta.y, }, })); }; const handleDragEnd = (event) => { // Update main state once (expensive) if (event.over && event.active.id !== event.over.id) { setItems(prevItems => { const oldIndex = prevItems.findIndex(i => i.id === event.active.id); const newIndex = prevItems.findIndex(i => i.id === event.over.id); return arrayMove(prevItems, oldIndex, newIndex); }); } // Clean up setDragPreview(null); setActiveId(null); }; return ( {/* Render items */} {/* Render preview overlay if dragging */} ); } ``` ## Large Dataset Handling ### Chunked Rendering ```tsx function ChunkedDragList({ items, chunkSize = 20 }) { const [renderedChunks, setRenderedChunks] = useState(1); useEffect(() => { // Progressively render chunks const timer = setTimeout(() => { if (renderedChunks * chunkSize < items.length) { setRenderedChunks(prev => prev + 1); } }, 0); return () => clearTimeout(timer); }, [renderedChunks, items.length, chunkSize]); const visibleItems = items.slice(0, renderedChunks * chunkSize); return (
{visibleItems.map(item => ( ))} {visibleItems.length < items.length && (
Rendering {visibleItems.length} of {items.length} items...
)}
); } ``` ### Indexed Data Structure ```tsx // Use Map for O(1) lookups instead of Array.find function useIndexedItems(initialItems: Item[]) { const [itemsMap, setItemsMap] = useState(() => { const map = new Map(); initialItems.forEach(item => map.set(item.id, item)); return map; }); const [itemOrder, setItemOrder] = useState(() => initialItems.map(item => item.id) ); const items = useMemo( () => itemOrder.map(id => itemsMap.get(id)!).filter(Boolean), [itemOrder, itemsMap] ); const moveItem = useCallback((fromId: string, toId: string) => { setItemOrder(prev => { const fromIndex = prev.indexOf(fromId); const toIndex = prev.indexOf(toId); if (fromIndex === -1 || toIndex === -1) return prev; const newOrder = [...prev]; const [removed] = newOrder.splice(fromIndex, 1); newOrder.splice(toIndex, 0, removed); return newOrder; }); }, []); const updateItem = useCallback((id: string, updates: Partial) => { setItemsMap(prev => { const newMap = new Map(prev); const item = newMap.get(id); if (item) { newMap.set(id, { ...item, ...updates }); } return newMap; }); }, []); return { items, moveItem, updateItem }; } ``` ## Memory Management ### Cleanup and Prevention ```tsx function useDragDropCleanup() { const activeRefs = useRef>(new Set()); const observers = useRef>(new Set()); const registerElement = useCallback((element: HTMLElement) => { activeRefs.current.add(element); return () => { activeRefs.current.delete(element); // Clean up any attached data delete (element as any)._dragData; }; }, []); const createObserver = useCallback((callback: IntersectionObserverCallback) => { const observer = new IntersectionObserver(callback); observers.current.add(observer); return observer; }, []); useEffect(() => { return () => { // Cleanup all refs activeRefs.current.forEach(element => { delete (element as any)._dragData; }); activeRefs.current.clear(); // Disconnect all observers observers.current.forEach(observer => observer.disconnect()); observers.current.clear(); }; }, []); return { registerElement, createObserver }; } ``` ### Memory-Efficient File Handling ```tsx function useFilePreview(file: File | null) { const [preview, setPreview] = useState(null); useEffect(() => { if (!file || !file.type.startsWith('image/')) { setPreview(null); return; } // Create preview URL const objectUrl = URL.createObjectURL(file); setPreview(objectUrl); // Cleanup function return () => { URL.revokeObjectURL(objectUrl); }; }, [file]); return preview; } // Batch cleanup for multiple files function useBatchFileCleanup() { const urlsRef = useRef>(new Set()); const createPreview = (file: File): string => { const url = URL.createObjectURL(file); urlsRef.current.add(url); return url; }; const cleanupPreview = (url: string) => { URL.revokeObjectURL(url); urlsRef.current.delete(url); }; const cleanupAll = () => { urlsRef.current.forEach(url => URL.revokeObjectURL(url)); urlsRef.current.clear(); }; useEffect(() => { return cleanupAll; }, []); return { createPreview, cleanupPreview, cleanupAll }; } ```