From fedc1d8d2bfd4ba1aa5d25e4e27783594fe193b3 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Mon, 1 Dec 2025 13:24:43 -0800 Subject: [PATCH] optimize spinner --- .../components/GooseSidebar/AppSidebar.tsx | 19 ++--- .../src/components/SessionIndicators.tsx | 63 +++++++++-------- .../src/components/ui/optimized-spinner.tsx | 70 +++++++++++++++++++ 3 files changed, 116 insertions(+), 36 deletions(-) create mode 100644 ui/desktop/src/components/ui/optimized-spinner.tsx diff --git a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx index f0953e5868..090745e2e6 100644 --- a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx +++ b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx @@ -280,18 +280,21 @@ const AppSidebar: React.FC = ({ currentPath }) => { return currentPath === path; }; - const handleNewChat = async () => { + const handleNewChat = React.useCallback(async () => { await startNewSession('', setView); - }; + }, [setView]); - const handleSessionClick = async (session: Session) => { - markSessionActive(session.id); - resumeSession(session, setView); - }; + const handleSessionClick = React.useCallback( + async (session: Session) => { + markSessionActive(session.id); + resumeSession(session, setView); + }, + [markSessionActive, setView] + ); - const handleViewAllClick = () => { + const handleViewAllClick = React.useCallback(() => { navigate('/sessions'); - }; + }, [navigate]); const renderMenuItem = (entry: NavigationEntry, index: number) => { if (entry.type === 'separator') { diff --git a/ui/desktop/src/components/SessionIndicators.tsx b/ui/desktop/src/components/SessionIndicators.tsx index 1e12041725..70bd011c54 100644 --- a/ui/desktop/src/components/SessionIndicators.tsx +++ b/ui/desktop/src/components/SessionIndicators.tsx @@ -1,4 +1,6 @@ -import { Loader2, AlertCircle } from 'lucide-react'; +import { AlertCircle } from 'lucide-react'; +import React from 'react'; +import { OptimizedSpinner } from './ui/optimized-spinner'; interface SessionIndicatorsProps { isStreaming?: boolean; @@ -10,33 +12,38 @@ interface SessionIndicatorsProps { /** * Visual indicators for session status * - Error: red alert icon (highest priority) - * - Streaming: animated spinner + * - Streaming: animated spinner with GPU acceleration * - Has activity: green dot (shown after streaming completes until viewed) */ -export function SessionIndicators({ - isStreaming, - hasUnread, - hasError, - className = '', -}: SessionIndicatorsProps) { - if (!isStreaming && !hasUnread && !hasError) { - return null; - } +export const SessionIndicators = React.memo( + ({ isStreaming, hasUnread, hasError, className = '' }) => { + if (!isStreaming && !hasUnread && !hasError) { + return null; + } - return ( -
- {hasError && ( - - )} - {isStreaming && !hasError && ( - - )} - {hasUnread && !isStreaming && !hasError && ( -
- )} -
- ); -} + return ( +
+ {hasError && ( + + )} + {isStreaming && !hasError && ( + + )} + {hasUnread && !isStreaming && !hasError && ( +
+ )} +
+ ); + } +); + +SessionIndicators.displayName = 'SessionIndicators'; diff --git a/ui/desktop/src/components/ui/optimized-spinner.tsx b/ui/desktop/src/components/ui/optimized-spinner.tsx new file mode 100644 index 0000000000..f6286a4a7d --- /dev/null +++ b/ui/desktop/src/components/ui/optimized-spinner.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { Loader2 } from 'lucide-react'; +import { cn } from '../../utils'; + +interface OptimizedSpinnerProps { + className?: string; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + color?: string; + 'aria-label'?: string; +} + +// Size mappings +const sizeClasses = { + xs: 'w-3 h-3', + sm: 'w-4 h-4', + md: 'w-6 h-6', + lg: 'w-8 h-8', + xl: 'w-10 h-10', +}; + +/** + * GPU-accelerated spinner component for smooth performance + * Uses CSS transforms and containment for optimal rendering + */ +export const OptimizedSpinner = React.memo( + ({ className = '', size = 'sm', color, 'aria-label': ariaLabel = 'Loading' }) => { + // Inject optimized keyframes on first mount + React.useEffect(() => { + const styleId = 'optimized-spin-keyframes'; + if (!document.getElementById(styleId)) { + const style = document.createElement('style'); + style.id = styleId; + style.textContent = ` + @keyframes optimized-spin { + from { + transform: rotate(0deg) translateZ(0); + } + to { + transform: rotate(360deg) translateZ(0); + } + } + + .spinner-container { + contain: layout style paint; + will-change: transform; + transform: translateZ(0); + backface-visibility: hidden; + perspective: 1000px; + } + `; + document.head.appendChild(style); + } + }, []); + + return ( +
+ +
+ ); + } +); + +OptimizedSpinner.displayName = 'OptimizedSpinner';