️(frontend) memoize the Placeholder component

Turn the Placeholder into a pure leaf component and memoize it, so
it does not re-render on unrelated parent updates when its props
have not changed.
This commit is contained in:
lebaudantoine
2026-07-23 18:24:15 +02:00
parent 13c9d3635c
commit a0274a2d54
2 changed files with 36 additions and 35 deletions
@@ -1,10 +1,7 @@
import type { Participant } from 'livekit-client'
import { styled } from '@/styled-system/jsx' import { styled } from '@/styled-system/jsx'
import { Avatar } from '@/components/Avatar' import { Avatar } from '@/components/Avatar'
import { useIsSpeaking } from '@livekit/components-react'
import { getParticipantBackgroundGradient } from '@/features/rooms/utils/getParticipantBackgroundGradient' import { getParticipantBackgroundGradient } from '@/features/rooms/utils/getParticipantBackgroundGradient'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor' import React, { useMemo } from 'react'
import { useMemo } from 'react'
const StyledParticipantPlaceHolder = styled('div', { const StyledParticipantPlaceHolder = styled('div', {
base: { base: {
@@ -23,39 +20,36 @@ const StyledAvatarWrapper = styled('div', {
aspectRatio: '1 / 1', aspectRatio: '1 / 1',
width: 'min(90cqmin, 160px)', width: 'min(90cqmin, 160px)',
fontSize: 'min(27cqmin, 48px)', fontSize: 'min(27cqmin, 48px)',
'[data-lk-speaking="true"] &': {
animation: 'pulse 1s infinite',
},
}, },
}) })
type ParticipantPlaceholderProps = { type ParticipantPlaceholderProps = {
participant: Participant color: string
displayedNamed: string
} }
export const ParticipantPlaceholder = ({ export const ParticipantPlaceholder = React.memo(
participant, ({ color, displayedNamed }: ParticipantPlaceholderProps) => {
}: ParticipantPlaceholderProps) => { const backgroundGradient = useMemo(
const isSpeaking = useIsSpeaking(participant) () => getParticipantBackgroundGradient(color),
const participantColor = getParticipantColor(participant) [color]
const backgroundGradient = useMemo( )
() => getParticipantBackgroundGradient(participantColor), return (
[participantColor] <StyledParticipantPlaceHolder
) style={{
backgroundColor: color,
return ( backgroundImage: backgroundGradient,
<StyledParticipantPlaceHolder }}
style={{
backgroundColor: participantColor,
backgroundImage: backgroundGradient,
}}
>
<StyledAvatarWrapper
style={{ animation: isSpeaking ? 'pulse 1s infinite' : undefined }}
> >
<Avatar <StyledAvatarWrapper>
name={participant.name} <Avatar name={displayedNamed} bgColor={color} context="placeholder" />
bgColor={participantColor} </StyledAvatarWrapper>
context="placeholder" </StyledParticipantPlaceHolder>
/> )
</StyledAvatarWrapper> }
</StyledParticipantPlaceHolder> )
)
} ParticipantPlaceholder.displayName = 'ParticipantPlaceholder'
@@ -1,3 +1,4 @@
import React from 'react'
import { import {
AudioTrack, AudioTrack,
ParticipantTileProps, ParticipantTileProps,
@@ -9,7 +10,6 @@ import {
TrackRefContext, TrackRefContext,
ParticipantContextIfNeeded, ParticipantContextIfNeeded,
} from '@livekit/components-react' } from '@livekit/components-react'
import React from 'react'
import { import {
isEqualTrackRef, isEqualTrackRef,
isTrackReference, isTrackReference,
@@ -26,6 +26,7 @@ import { formatShortcutLabel } from '@/features/shortcuts/formatLabels'
import { KeyboardShortcutHint } from './KeyboardShortcutHint' import { KeyboardShortcutHint } from './KeyboardShortcutHint'
import { layoutStore, clearPinnedTrack } from '@/stores/layout' import { layoutStore, clearPinnedTrack } from '@/stores/layout'
import { ParticipantMetadata } from './ParticipantMetadata' import { ParticipantMetadata } from './ParticipantMetadata'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
export function TrackRefContextIfNeeded( export function TrackRefContextIfNeeded(
props: React.PropsWithChildren<{ props: React.PropsWithChildren<{
@@ -90,6 +91,8 @@ export const ParticipantTile: (
const isScreenShare = trackReference.source != Track.Source.Camera const isScreenShare = trackReference.source != Track.Source.Camera
const [hasKeyboardFocus, setHasKeyboardFocus] = React.useState(false) const [hasKeyboardFocus, setHasKeyboardFocus] = React.useState(false)
const participantColor = getParticipantColor(trackReference.participant)
const participantName = getParticipantName(trackReference.participant) const participantName = getParticipantName(trackReference.participant)
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' }) const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
@@ -141,7 +144,11 @@ export const ParticipantTile: (
)} )}
<div className="lk-participant-placeholder"> <div className="lk-participant-placeholder">
<ParticipantPlaceholder <ParticipantPlaceholder
participant={trackReference.participant} color={participantColor}
displayedNamed={
trackReference.participant.name ||
trackReference.participant.identity
}
/> />
</div> </div>
{!disableMetadata && ( {!disableMetadata && (