From c9342f833bf2f036ff1d10a1c1e5680f266a0cce Mon Sep 17 00:00:00 2001 From: Kirill Date: Sun, 24 May 2026 19:44:50 +0500 Subject: [PATCH] perf: add React.memo to hot-path components --- client/src/app/layout/AppHeader.tsx | 3 ++- client/src/entities/product/ui/ProductCard.tsx | 7 ++++++- client/src/shared/ui/OptimizedImage.tsx | 3 ++- client/src/shared/ui/UserAvatar.tsx | 3 ++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/client/src/app/layout/AppHeader.tsx b/client/src/app/layout/AppHeader.tsx index d3c97de..a9606a1 100644 --- a/client/src/app/layout/AppHeader.tsx +++ b/client/src/app/layout/AppHeader.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import React from 'react' import AppBar from '@mui/material/AppBar' import Badge from '@mui/material/Badge' import Box from '@mui/material/Box' @@ -30,7 +31,7 @@ type NavItem = { label: string; to: string } const navItems: NavItem[] = [{ label: 'Каталог', to: '/' }] -export function AppHeader() { +export const AppHeader = React.memo(function AppHeader() { const { mode, resolvedMode, scheme, setScheme, cycleMode } = useThemeController() const user = useUnit($user) const navigate = useNavigate() diff --git a/client/src/entities/product/ui/ProductCard.tsx b/client/src/entities/product/ui/ProductCard.tsx index 0322d3a..ca16f09 100644 --- a/client/src/entities/product/ui/ProductCard.tsx +++ b/client/src/entities/product/ui/ProductCard.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react' import { useCallback, useMemo, useRef } from 'react' +import React from 'react' import { useMediaQuery } from '@mui/material' import Box from '@mui/material/Box' import Card from '@mui/material/Card' @@ -18,7 +19,7 @@ import type { Swiper as SwiperType } from 'swiper/types' type Props = { product: Product; mediaHeight?: number; actions?: ReactNode } -export function ProductCard({ product, mediaHeight = 200, actions }: Props) { +const ProductCardInner = ({ product, mediaHeight = 200, actions }: Props) => { const navigate = useNavigate() const isMobile = useMediaQuery('(max-width:600px)') const swiperRef = useRef(null) @@ -247,3 +248,7 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) { ) } + +export const ProductCard = React.memo(ProductCardInner, (prev, next) => { + return prev.product.id === next.product.id && prev.mediaHeight === next.mediaHeight && prev.actions === next.actions +}) diff --git a/client/src/shared/ui/OptimizedImage.tsx b/client/src/shared/ui/OptimizedImage.tsx index 85da53a..89b0c70 100644 --- a/client/src/shared/ui/OptimizedImage.tsx +++ b/client/src/shared/ui/OptimizedImage.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react' +import React from 'react' import Box from '@mui/material/Box' import type { SxProps, Theme } from '@mui/material/styles' @@ -35,7 +36,7 @@ function buildFallbackSrc(src: string, width: number): string { return `/uploads-resized/${pathPrefix}${parsed.uuid}.webp?w=${width}` } -export function OptimizedImage({ +export const OptimizedImage = React.memo(function OptimizedImage({ src, alt, widths = DEFAULT_WIDTHS, diff --git a/client/src/shared/ui/UserAvatar.tsx b/client/src/shared/ui/UserAvatar.tsx index cda9ef8..3f91339 100644 --- a/client/src/shared/ui/UserAvatar.tsx +++ b/client/src/shared/ui/UserAvatar.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react' +import React from 'react' import Avatar from '@mui/material/Avatar' import type { SxProps, Theme } from '@mui/material/styles' import { createAvatar } from '@dicebear/core' @@ -12,7 +13,7 @@ type UserAvatarProps = { sx?: SxProps } -export function UserAvatar({ userId, avatarUrl, avatarStyle, size = 40, sx }: UserAvatarProps) { +export const UserAvatar = React.memo(function UserAvatar({ userId, avatarUrl, avatarStyle, size = 40, sx }: UserAvatarProps) { const [generatedSrc, setGeneratedSrc] = useState(null) const styleId = avatarStyle || DEFAULT_STYLE_ID const styleIdRef = useRef(styleId)