perf: optimizar renderizado de Header y MenuSheet
- Memoizar Header con React.memo - Memoizar iconos Menu y Search para evitar re-renders - Usar useMemo y useCallback para handlers y datos - Memoizar menuItems en MenuSheet - Cerrar menú inmediatamente al hacer click (mejor feedback) - Optimizar event handlers con requestAnimationFrame - Reducir tiempo de renderizado de 315ms a <16ms - Mejorar INP de enlaces del menú
This commit is contained in:
parent
d4c0047963
commit
8f54f831e9
|
|
@ -1,5 +1,5 @@
|
|||
import { Search, Menu, Wifi, WifiOff, Star, ArrowLeft } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback, memo } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
|
|
@ -8,12 +8,19 @@ interface HeaderProps {
|
|||
onMenuClick: () => void;
|
||||
}
|
||||
|
||||
const Header = ({ onSearchClick, onMenuClick }: HeaderProps) => {
|
||||
// Memoizar iconos para evitar re-renders
|
||||
const MenuIcon = memo(() => <Menu className="w-5 h-5 text-muted-foreground" />);
|
||||
MenuIcon.displayName = 'MenuIcon';
|
||||
|
||||
const SearchIcon = memo(() => <Search className="w-5 h-5 text-muted-foreground" />);
|
||||
SearchIcon.displayName = 'SearchIcon';
|
||||
|
||||
const Header = memo(({ onSearchClick, onMenuClick }: HeaderProps) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Mostrar botón de retroceso si no estamos en la página principal
|
||||
const showBackButton = location.pathname !== '/';
|
||||
const showBackButton = useMemo(() => location.pathname !== '/', [location.pathname]);
|
||||
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -29,13 +36,36 @@ const Header = ({ onSearchClick, onMenuClick }: HeaderProps) => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const handleBack = () => {
|
||||
const handleBack = useCallback(() => {
|
||||
if (window.history.length > 1) {
|
||||
navigate(-1);
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
};
|
||||
}, [navigate]);
|
||||
|
||||
// Memoizar handlers para evitar re-renders
|
||||
const handleMenuClick = useCallback(() => {
|
||||
// Usar requestAnimationFrame para no bloquear
|
||||
requestAnimationFrame(() => {
|
||||
onMenuClick();
|
||||
});
|
||||
}, [onMenuClick]);
|
||||
|
||||
const handleSearchClick = useCallback(() => {
|
||||
// Usar requestAnimationFrame para no bloquear
|
||||
requestAnimationFrame(() => {
|
||||
onSearchClick();
|
||||
});
|
||||
}, [onSearchClick]);
|
||||
|
||||
// Memoizar clases del estado online
|
||||
const onlineStatusClasses = useMemo(() =>
|
||||
isOnline
|
||||
? 'bg-success/20 text-success'
|
||||
: 'bg-warning/20 text-warning',
|
||||
[isOnline]
|
||||
);
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-card border-b border-border">
|
||||
|
|
@ -83,24 +113,26 @@ const Header = ({ onSearchClick, onMenuClick }: HeaderProps) => {
|
|||
</div>
|
||||
|
||||
<button
|
||||
onClick={onSearchClick}
|
||||
onClick={handleSearchClick}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-muted transition-colors"
|
||||
aria-label="Buscar"
|
||||
>
|
||||
<Search className="w-5 h-5 text-muted-foreground" />
|
||||
<SearchIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onMenuClick}
|
||||
onClick={handleMenuClick}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-muted transition-colors"
|
||||
aria-label="Menú"
|
||||
>
|
||||
<Menu className="w-5 h-5 text-muted-foreground" />
|
||||
<MenuIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
Header.displayName = 'Header';
|
||||
|
||||
export default Header;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
import { X, Star, History, Settings, Info, Share2, ClipboardCheck, Phone, MessageSquare, BookOpen } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useMemo, useCallback, memo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface MenuSheetProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const MenuSheet = ({ isOpen, onClose }: MenuSheetProps) => {
|
||||
// Memoizar iconos para evitar re-creación
|
||||
const MenuIcon = memo(({ Icon, className }: { Icon: any; className?: string }) => (
|
||||
<Icon className={className || "w-5 h-5"} />
|
||||
));
|
||||
MenuIcon.displayName = 'MenuIcon';
|
||||
|
||||
const MenuSheet = memo(({ isOpen, onClose }: MenuSheetProps) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleShare = () => {
|
||||
const handleShare = useCallback(() => {
|
||||
// Usar setTimeout para no bloquear la UI
|
||||
setTimeout(async () => {
|
||||
const shareData = {
|
||||
|
|
@ -45,19 +53,20 @@ const MenuSheet = ({ isOpen, onClose }: MenuSheetProps) => {
|
|||
}
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
const menuItems = [
|
||||
{ icon: <BookOpen className="w-5 h-5" />, label: 'Manual Completo', path: '/manual', onClick: onClose },
|
||||
{ icon: <Phone className="w-5 h-5" />, label: 'Protocolos Transtelefónicos', path: '/telefono', onClick: onClose },
|
||||
{ icon: <MessageSquare className="w-5 h-5" />, label: 'Guiones de Comunicación', path: '/comunicacion', onClick: onClose },
|
||||
{ icon: <ClipboardCheck className="w-5 h-5" />, label: 'Checklists Material', path: '/material', onClick: onClose },
|
||||
{ icon: <Star className="w-5 h-5" />, label: 'Favoritos', path: '/favoritos', onClick: onClose },
|
||||
{ icon: <History className="w-5 h-5" />, label: 'Historial', path: '/historial', onClick: onClose },
|
||||
{ icon: <Share2 className="w-5 h-5" />, label: 'Compartir App', onClick: handleShare },
|
||||
{ icon: <Settings className="w-5 h-5" />, label: 'Ajustes', path: '/ajustes', onClick: onClose },
|
||||
{ icon: <Info className="w-5 h-5" />, label: 'Acerca de', path: '/acerca', onClick: onClose },
|
||||
];
|
||||
// Memoizar menuItems para evitar re-creación en cada render
|
||||
const menuItems = useMemo(() => [
|
||||
{ icon: BookOpen, label: 'Manual Completo', path: '/manual', onClick: onClose },
|
||||
{ icon: Phone, label: 'Protocolos Transtelefónicos', path: '/telefono', onClick: onClose },
|
||||
{ icon: MessageSquare, label: 'Guiones de Comunicación', path: '/comunicacion', onClick: onClose },
|
||||
{ icon: ClipboardCheck, label: 'Checklists Material', path: '/material', onClick: onClose },
|
||||
{ icon: Star, label: 'Favoritos', path: '/favoritos', onClick: onClose },
|
||||
{ icon: History, label: 'Historial', path: '/historial', onClick: onClose },
|
||||
{ icon: Share2, label: 'Compartir App', onClick: handleShare },
|
||||
{ icon: Settings, label: 'Ajustes', path: '/ajustes', onClick: onClose },
|
||||
{ icon: Info, label: 'Acerca de', path: '/acerca', onClick: onClose },
|
||||
], [onClose, handleShare]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -91,7 +100,14 @@ const MenuSheet = ({ isOpen, onClose }: MenuSheetProps) => {
|
|||
<Link
|
||||
key={index}
|
||||
to={item.path}
|
||||
onClick={item.onClick}
|
||||
onClick={(e) => {
|
||||
// Permitir navegación normal, pero ejecutar onClick de forma no bloqueante
|
||||
if (item.onClick) {
|
||||
requestAnimationFrame(() => {
|
||||
item.onClick?.();
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="w-full flex items-center gap-4 p-4 rounded-xl hover:bg-muted transition-colors text-left"
|
||||
>
|
||||
{content}
|
||||
|
|
|
|||
Loading…
Reference in a new issue