perf: dynamic import dicebear avatar styles

This commit is contained in:
Kirill
2026-05-24 19:42:17 +05:00
parent 8a4fd53bc4
commit 0dd5f8b8ff
4 changed files with 75 additions and 95 deletions
@@ -15,7 +15,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useUnit } from 'effector-react' import { useUnit } from 'effector-react'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { apiClient } from '@/shared/api/client' import { apiClient } from '@/shared/api/client'
import { AVATAR_STYLES, DEFAULT_STYLE_ID, getStyleById } from '@/shared/lib/avatar-styles' import { AVATAR_STYLE_LOADERS, DEFAULT_STYLE_ID, loadAvatarStyle } from '@/shared/lib/avatar-styles'
import { $user, updateProfileFx } from '@/shared/model/auth' import { $user, updateProfileFx } from '@/shared/model/auth'
import type { UpdateProfileParams } from '@/shared/model/auth' import type { UpdateProfileParams } from '@/shared/model/auth'
import { UserAvatar } from '@/shared/ui/UserAvatar' import { UserAvatar } from '@/shared/ui/UserAvatar'
@@ -165,7 +165,7 @@ export function AdminSettingsPage() {
<FormControl size="small" sx={{ minWidth: 140 }}> <FormControl size="small" sx={{ minWidth: 140 }}>
<InputLabel>Стиль</InputLabel> <InputLabel>Стиль</InputLabel>
<Select value={selectedStyle} label="Стиль" onChange={(e) => setSelectedStyle(e.target.value)}> <Select value={selectedStyle} label="Стиль" onChange={(e) => setSelectedStyle(e.target.value)}>
{AVATAR_STYLES.map((s) => ( {AVATAR_STYLE_LOADERS.map((s) => (
<MenuItem key={s.id} value={s.id}> <MenuItem key={s.id} value={s.id}>
{s.label} {s.label}
</MenuItem> </MenuItem>
@@ -174,10 +174,10 @@ export function AdminSettingsPage() {
</FormControl> </FormControl>
<Button <Button
variant="outlined" variant="outlined"
onClick={() => { onClick={async () => {
const seed = `${String(user.id)}_${Date.now()}` const seed = `${String(user.id)}_${Date.now()}`
const styleDef = getStyleById(selectedStyle) const style = await loadAvatarStyle(selectedStyle)
const avatar = createAvatar(styleDef.style, { seed }) const avatar = createAvatar(style, { seed })
setPreviewSrc(avatar.toDataUri()) setPreviewSrc(avatar.toDataUri())
setPreviewStyle(selectedStyle) setPreviewStyle(selectedStyle)
}} }}
@@ -9,7 +9,7 @@ import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { createAvatar } from '@dicebear/core' import { createAvatar } from '@dicebear/core'
import { useUnit } from 'effector-react' import { useUnit } from 'effector-react'
import { AVATAR_STYLES, DEFAULT_STYLE_ID, getStyleById } from '@/shared/lib/avatar-styles' import { AVATAR_STYLE_LOADERS, DEFAULT_STYLE_ID, loadAvatarStyle } from '@/shared/lib/avatar-styles'
import { $user, updateProfileFx } from '@/shared/model/auth' import { $user, updateProfileFx } from '@/shared/model/auth'
import { UserAvatar } from '@/shared/ui/UserAvatar' import { UserAvatar } from '@/shared/ui/UserAvatar'
@@ -72,7 +72,7 @@ export function AvatarSection() {
label="Стиль" label="Стиль"
onChange={(e) => setSelectedStyle(e.target.value)} onChange={(e) => setSelectedStyle(e.target.value)}
> >
{AVATAR_STYLES.map((s) => ( {AVATAR_STYLE_LOADERS.map((s) => (
<MenuItem key={s.id} value={s.id}> <MenuItem key={s.id} value={s.id}>
{s.label} {s.label}
</MenuItem> </MenuItem>
@@ -81,10 +81,10 @@ export function AvatarSection() {
</FormControl> </FormControl>
<Button <Button
variant="outlined" variant="outlined"
onClick={() => { onClick={async () => {
const seed = `${user.id}_${Date.now()}` const seed = `${user.id}_${Date.now()}`
const styleDef = getStyleById(selectedStyle) const style = await loadAvatarStyle(selectedStyle)
const avatar = createAvatar(styleDef.style, { seed }) const avatar = createAvatar(style, { seed })
setPreviewSrc(avatar.toDataUri()) setPreviewSrc(avatar.toDataUri())
setPreviewStyle(selectedStyle) setPreviewStyle(selectedStyle)
}} }}
+42 -76
View File
@@ -1,88 +1,54 @@
import { create as adventurerCreate, meta as adventurerMeta, schema as adventurerSchema } from '@dicebear/adventurer'
import { create as avataaarsCreate, meta as avataaarsMeta, schema as avataaarsSchema } from '@dicebear/avataaars'
import { create as bigEarsCreate, meta as bigEarsMeta, schema as bigEarsSchema } from '@dicebear/big-ears'
import { create as bigSmileCreate, meta as bigSmileMeta, schema as bigSmileSchema } from '@dicebear/big-smile'
import { create as botttsCreate, meta as botttsMeta, schema as botttsSchema } from '@dicebear/bottts'
import { create as croodlesCreate, meta as croodlesMeta, schema as croodlesSchema } from '@dicebear/croodles'
import { create as funEmojiCreate, meta as funEmojiMeta, schema as funEmojiSchema } from '@dicebear/fun-emoji'
import { create as identiconCreate, meta as identiconMeta, schema as identiconSchema } from '@dicebear/identicon'
import { create as initialsCreate, meta as initialsMeta, schema as initialsSchema } from '@dicebear/initials'
import { create as loreleiCreate, meta as loreleiMeta, schema as loreleiSchema } from '@dicebear/lorelei'
import { create as micahCreate, meta as micahMeta, schema as micahSchema } from '@dicebear/micah'
import { create as notionistsCreate, meta as notionistsMeta, schema as notionistsSchema } from '@dicebear/notionists'
import { create as pixelArtCreate, meta as pixelArtMeta, schema as pixelArtSchema } from '@dicebear/pixel-art'
import { create as ringsCreate, meta as ringsMeta, schema as ringsSchema } from '@dicebear/rings'
import { create as shapesCreate, meta as shapesMeta, schema as shapesSchema } from '@dicebear/shapes'
import { create as thumbsCreate, meta as thumbsMeta, schema as thumbsSchema } from '@dicebear/thumbs'
import type { Style } from '@dicebear/core' import type { Style } from '@dicebear/core'
type StyleDef = { type StyleDef = {
id: string id: string
label: string label: string
style: Style<any> loader: () => Promise<{ create: any; meta: any; schema: any }>
} }
export const AVATAR_STYLES: StyleDef[] = [ export const AVATAR_STYLE_LOADERS: StyleDef[] = [
{ id: 'bottts', label: 'Роботы', style: { create: botttsCreate, meta: botttsMeta, schema: botttsSchema } }, { id: 'bottts', label: 'Роботы', loader: () => import('@dicebear/bottts') },
{ { id: 'identicon', label: 'Узоры', loader: () => import('@dicebear/identicon') },
id: 'identicon', { id: 'avataaars', label: 'Персонажи', loader: () => import('@dicebear/avataaars') },
label: 'Узоры', { id: 'notionists', label: 'Notion', loader: () => import('@dicebear/notionists') },
style: { create: identiconCreate, meta: identiconMeta, schema: identiconSchema }, { id: 'thumbs', label: 'Thumbs', loader: () => import('@dicebear/thumbs') },
}, { id: 'lorelei', label: 'Lorelei', loader: () => import('@dicebear/lorelei') },
{ { id: 'micah', label: 'Micah', loader: () => import('@dicebear/micah') },
id: 'avataaars', { id: 'pixel-art', label: 'Пиксели', loader: () => import('@dicebear/pixel-art') },
label: 'Персонажи', { id: 'rings', label: 'Кольца', loader: () => import('@dicebear/rings') },
style: { create: avataaarsCreate, meta: avataaarsMeta, schema: avataaarsSchema }, { id: 'shapes', label: 'Фигуры', loader: () => import('@dicebear/shapes') },
}, { id: 'initials', label: 'Инициалы', loader: () => import('@dicebear/initials') },
{ { id: 'adventurer', label: 'Adventurer', loader: () => import('@dicebear/adventurer') },
id: 'notionists', { id: 'big-ears', label: 'Big Ears', loader: () => import('@dicebear/big-ears') },
label: 'Notion', { id: 'big-smile', label: 'Big Smile', loader: () => import('@dicebear/big-smile') },
style: { create: notionistsCreate, meta: notionistsMeta, schema: notionistsSchema }, { id: 'croodles', label: 'Croodles', loader: () => import('@dicebear/croodles') },
}, { id: 'fun-emoji', label: 'Fun Emoji', loader: () => import('@dicebear/fun-emoji') },
{ id: 'thumbs', label: 'Thumbs', style: { create: thumbsCreate, meta: thumbsMeta, schema: thumbsSchema } },
{ id: 'lorelei', label: 'Lorelei', style: { create: loreleiCreate, meta: loreleiMeta, schema: loreleiSchema } },
{ id: 'micah', label: 'Micah', style: { create: micahCreate, meta: micahMeta, schema: micahSchema } },
{
id: 'pixel-art',
label: 'Пиксели',
style: { create: pixelArtCreate, meta: pixelArtMeta, schema: pixelArtSchema },
},
{ id: 'rings', label: 'Кольца', style: { create: ringsCreate, meta: ringsMeta, schema: ringsSchema } },
{ id: 'shapes', label: 'Фигуры', style: { create: shapesCreate, meta: shapesMeta, schema: shapesSchema } },
{
id: 'initials',
label: 'Инициалы',
style: { create: initialsCreate, meta: initialsMeta, schema: initialsSchema },
},
{
id: 'adventurer',
label: 'Adventurer',
style: { create: adventurerCreate, meta: adventurerMeta, schema: adventurerSchema },
},
{
id: 'big-ears',
label: 'Big Ears',
style: { create: bigEarsCreate, meta: bigEarsMeta, schema: bigEarsSchema },
},
{
id: 'big-smile',
label: 'Big Smile',
style: { create: bigSmileCreate, meta: bigSmileMeta, schema: bigSmileSchema },
},
{
id: 'croodles',
label: 'Croodles',
style: { create: croodlesCreate, meta: croodlesMeta, schema: croodlesSchema },
},
{
id: 'fun-emoji',
label: 'Fun Emoji',
style: { create: funEmojiCreate, meta: funEmojiMeta, schema: funEmojiSchema },
},
] ]
export const DEFAULT_STYLE_ID = 'avataaars' export const DEFAULT_STYLE_ID = 'avataaars'
export function getStyleById(id: string | null | undefined): StyleDef { const styleCache = new Map<string, Style<any>>()
return AVATAR_STYLES.find((s) => s.id === id) ?? AVATAR_STYLES[0]
export async function loadAvatarStyle(id: string): Promise<Style<any>> {
if (styleCache.has(id)) {
return styleCache.get(id)!
}
const loader = AVATAR_STYLE_LOADERS.find((s) => s.id === id)
if (!loader) {
const fallback = AVATAR_STYLE_LOADERS.find((s) => s.id === DEFAULT_STYLE_ID)!
const mod = await fallback.loader()
const style = { create: mod.create, meta: mod.meta, schema: mod.schema }
styleCache.set(DEFAULT_STYLE_ID, style)
return style
}
const mod = await loader.loader()
const style = { create: mod.create, meta: mod.meta, schema: mod.schema }
styleCache.set(id, style)
return style
}
export function getStyleLabel(id: string): string {
return AVATAR_STYLE_LOADERS.find((s) => s.id === id)?.label ?? id
} }
+23 -9
View File
@@ -1,8 +1,8 @@
import { useMemo } from 'react' import { useEffect, useRef, useState } from 'react'
import Avatar from '@mui/material/Avatar' import Avatar from '@mui/material/Avatar'
import type { SxProps, Theme } from '@mui/material/styles' import type { SxProps, Theme } from '@mui/material/styles'
import { createAvatar } from '@dicebear/core' import { createAvatar } from '@dicebear/core'
import { DEFAULT_STYLE_ID, getStyleById } from '@/shared/lib/avatar-styles' import { DEFAULT_STYLE_ID, loadAvatarStyle } from '@/shared/lib/avatar-styles'
type UserAvatarProps = { type UserAvatarProps = {
userId: string userId: string
@@ -13,17 +13,31 @@ type UserAvatarProps = {
} }
export function UserAvatar({ userId, avatarUrl, avatarStyle, size = 40, sx }: UserAvatarProps) { export function UserAvatar({ userId, avatarUrl, avatarStyle, size = 40, sx }: UserAvatarProps) {
const generatedSrc = useMemo(() => { const [generatedSrc, setGeneratedSrc] = useState<string | null>(null)
const styleDef = getStyleById(avatarStyle || DEFAULT_STYLE_ID) const styleId = avatarStyle || DEFAULT_STYLE_ID
const avatar = createAvatar(styleDef.style, { seed: userId }) const styleIdRef = useRef(styleId)
return avatar.toDataUri()
}, [userId, avatarStyle])
const src = avatarUrl || generatedSrc useEffect(() => {
let cancelled = false
styleIdRef.current = styleId
loadAvatarStyle(styleId).then((style) => {
if (!cancelled && styleIdRef.current === styleId) {
const avatar = createAvatar(style, { seed: userId })
setGeneratedSrc(avatar.toDataUri())
}
})
return () => {
cancelled = true
}
}, [userId, styleId])
const src = avatarUrl || generatedSrc || ''
return ( return (
<Avatar src={src} sx={{ width: size, height: size, bgcolor: 'primary.main', ...sx }}> <Avatar src={src} sx={{ width: size, height: size, bgcolor: 'primary.main', ...sx }}>
? {!src && '?'}
</Avatar> </Avatar>
) )
} }