️(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 { Avatar } from '@/components/Avatar'
import { useIsSpeaking } from '@livekit/components-react'
import { getParticipantBackgroundGradient } from '@/features/rooms/utils/getParticipantBackgroundGradient'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { useMemo } from 'react'
import React, { useMemo } from 'react'
const StyledParticipantPlaceHolder = styled('div', {
base: {
@@ -23,39 +20,36 @@ const StyledAvatarWrapper = styled('div', {
aspectRatio: '1 / 1',
width: 'min(90cqmin, 160px)',
fontSize: 'min(27cqmin, 48px)',
'[data-lk-speaking="true"] &': {
animation: 'pulse 1s infinite',
},
},
})
type ParticipantPlaceholderProps = {
participant: Participant
color: string
displayedNamed: string
}
export const ParticipantPlaceholder = ({
participant,
}: ParticipantPlaceholderProps) => {
const isSpeaking = useIsSpeaking(participant)
const participantColor = getParticipantColor(participant)
const backgroundGradient = useMemo(
() => getParticipantBackgroundGradient(participantColor),
[participantColor]
)
return (
<StyledParticipantPlaceHolder
style={{
backgroundColor: participantColor,
backgroundImage: backgroundGradient,
}}
>
<StyledAvatarWrapper
style={{ animation: isSpeaking ? 'pulse 1s infinite' : undefined }}
export const ParticipantPlaceholder = React.memo(
({ color, displayedNamed }: ParticipantPlaceholderProps) => {
const backgroundGradient = useMemo(
() => getParticipantBackgroundGradient(color),
[color]
)
return (
<StyledParticipantPlaceHolder
style={{
backgroundColor: color,
backgroundImage: backgroundGradient,
}}
>
<Avatar
name={participant.name}
bgColor={participantColor}
context="placeholder"
/>
</StyledAvatarWrapper>
</StyledParticipantPlaceHolder>
)
}
<StyledAvatarWrapper>
<Avatar name={displayedNamed} bgColor={color} context="placeholder" />
</StyledAvatarWrapper>
</StyledParticipantPlaceHolder>
)
}
)
ParticipantPlaceholder.displayName = 'ParticipantPlaceholder'
@@ -1,3 +1,4 @@
import React from 'react'
import {
AudioTrack,
ParticipantTileProps,
@@ -9,7 +10,6 @@ import {
TrackRefContext,
ParticipantContextIfNeeded,
} from '@livekit/components-react'
import React from 'react'
import {
isEqualTrackRef,
isTrackReference,
@@ -26,6 +26,7 @@ import { formatShortcutLabel } from '@/features/shortcuts/formatLabels'
import { KeyboardShortcutHint } from './KeyboardShortcutHint'
import { layoutStore, clearPinnedTrack } from '@/stores/layout'
import { ParticipantMetadata } from './ParticipantMetadata'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
export function TrackRefContextIfNeeded(
props: React.PropsWithChildren<{
@@ -90,6 +91,8 @@ export const ParticipantTile: (
const isScreenShare = trackReference.source != Track.Source.Camera
const [hasKeyboardFocus, setHasKeyboardFocus] = React.useState(false)
const participantColor = getParticipantColor(trackReference.participant)
const participantName = getParticipantName(trackReference.participant)
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
@@ -141,7 +144,11 @@ export const ParticipantTile: (
)}
<div className="lk-participant-placeholder">
<ParticipantPlaceholder
participant={trackReference.participant}
color={participantColor}
displayedNamed={
trackReference.participant.name ||
trackReference.participant.identity
}
/>
</div>
{!disableMetadata && (