mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 11:58:53 +00:00
⚡️(frontend) optimize re-renders of the CarouselLayout
Apply the same pattern as previous layouts: push state and useSize subscriptions down into child components. The CarouselLayout now only re-renders when maxVisibles or orientation actually change, instead of on every size update.
This commit is contained in:
committed by
aleb_the_flash
parent
ef05c0ab19
commit
eced9891c6
@@ -3,6 +3,7 @@ import { getScrollBarWidth } from '@livekit/components-core'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TrackLoop, useVisualStableUpdate } from '@livekit/components-react'
|
import { TrackLoop, useVisualStableUpdate } from '@livekit/components-react'
|
||||||
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
|
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
|
||||||
|
import { useCallback, useEffect, useLayoutEffect } from 'react'
|
||||||
|
|
||||||
const MIN_HEIGHT = 130
|
const MIN_HEIGHT = 130
|
||||||
const MIN_WIDTH = 140
|
const MIN_WIDTH = 140
|
||||||
@@ -10,6 +11,72 @@ const MIN_VISIBLE_TILES = 1
|
|||||||
const ASPECT_RATIO = 16 / 10
|
const ASPECT_RATIO = 16 / 10
|
||||||
const ASPECT_RATIO_INVERT = (1 - ASPECT_RATIO) * -1
|
const ASPECT_RATIO_INVERT = (1 - ASPECT_RATIO) * -1
|
||||||
|
|
||||||
|
type CarouselOrientation = 'vertical' | 'horizontal'
|
||||||
|
|
||||||
|
interface CarouselLayoutState {
|
||||||
|
orientation: CarouselOrientation
|
||||||
|
maxVisibleTiles: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CarouselLayoutObserverProps {
|
||||||
|
asideEl: React.RefObject<HTMLDivElement>
|
||||||
|
orientation?: CarouselOrientation
|
||||||
|
onLayoutChange: (layout: CarouselLayoutState) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CarouselLayoutObserver = ({
|
||||||
|
orientation,
|
||||||
|
asideEl,
|
||||||
|
onLayoutChange,
|
||||||
|
}: CarouselLayoutObserverProps) => {
|
||||||
|
const { width, height } = useSize(asideEl)
|
||||||
|
|
||||||
|
// Hysteresis memory: avoids flapping between N and N+1 tiles when the
|
||||||
|
// container size hovers around a breakpoint. A ref (not state) because
|
||||||
|
// updating it must not trigger a re-render.
|
||||||
|
const prevTilesRef = React.useRef(0)
|
||||||
|
|
||||||
|
const carouselOrientation: CarouselOrientation =
|
||||||
|
orientation ?? (height >= width ? 'vertical' : 'horizontal')
|
||||||
|
|
||||||
|
const tileSpan =
|
||||||
|
carouselOrientation === 'vertical'
|
||||||
|
? Math.max(width * ASPECT_RATIO_INVERT, MIN_HEIGHT)
|
||||||
|
: Math.max(height * ASPECT_RATIO, MIN_WIDTH)
|
||||||
|
|
||||||
|
const scrollBarWidth = getScrollBarWidth()
|
||||||
|
|
||||||
|
const availableSpan =
|
||||||
|
(carouselOrientation === 'vertical' ? height : width) - scrollBarWidth
|
||||||
|
|
||||||
|
const tilesThatFit = Math.max(availableSpan / tileSpan, MIN_VISIBLE_TILES)
|
||||||
|
|
||||||
|
let maxVisibleTiles: number
|
||||||
|
if (Math.abs(tilesThatFit - prevTilesRef.current) < 0.5) {
|
||||||
|
// Within the dead zone: keep the previous count.
|
||||||
|
maxVisibleTiles = Math.round(prevTilesRef.current)
|
||||||
|
} else {
|
||||||
|
maxVisibleTiles = Math.round(tilesThatFit)
|
||||||
|
prevTilesRef.current = tilesThatFit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply cosmetic layout output straight to the DOM.
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = asideEl.current
|
||||||
|
if (!el) return
|
||||||
|
el.dataset.lkOrientation = carouselOrientation
|
||||||
|
el.style.setProperty('--lk-max-visible-tiles', maxVisibleTiles.toString())
|
||||||
|
}, [asideEl, carouselOrientation, maxVisibleTiles])
|
||||||
|
|
||||||
|
// Report upward only what the parent actually needs for
|
||||||
|
// `useVisualStableUpdate` (and only when it changes — see parent handler).
|
||||||
|
useEffect(() => {
|
||||||
|
onLayoutChange({ orientation: carouselOrientation, maxVisibleTiles })
|
||||||
|
}, [carouselOrientation, maxVisibleTiles, onLayoutChange])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export interface CarouselLayoutProps extends React.HTMLAttributes<HTMLMediaElement> {
|
export interface CarouselLayoutProps extends React.HTMLAttributes<HTMLMediaElement> {
|
||||||
tracks: TrackReferenceOrPlaceholder[]
|
tracks: TrackReferenceOrPlaceholder[]
|
||||||
@@ -40,52 +107,42 @@ export function CarouselLayout({
|
|||||||
...props
|
...props
|
||||||
}: CarouselLayoutProps) {
|
}: CarouselLayoutProps) {
|
||||||
const asideEl = React.useRef<HTMLDivElement>(null)
|
const asideEl = React.useRef<HTMLDivElement>(null)
|
||||||
const [prevTiles, setPrevTiles] = React.useState(0)
|
|
||||||
const { width, height } = useSize(asideEl)
|
|
||||||
const carouselOrientation = orientation
|
|
||||||
? orientation
|
|
||||||
: height >= width
|
|
||||||
? 'vertical'
|
|
||||||
: 'horizontal'
|
|
||||||
|
|
||||||
const tileSpan =
|
const [layout, setLayout] = React.useState<CarouselLayoutState>({
|
||||||
carouselOrientation === 'vertical'
|
orientation: orientation ?? 'vertical',
|
||||||
? Math.max(width * ASPECT_RATIO_INVERT, MIN_HEIGHT)
|
maxVisibleTiles: MIN_VISIBLE_TILES,
|
||||||
: Math.max(height * ASPECT_RATIO, MIN_WIDTH)
|
})
|
||||||
const scrollBarWidth = getScrollBarWidth()
|
|
||||||
|
|
||||||
const tilesThatFit =
|
// Stable callback + identity check: the parent only re-renders when the
|
||||||
carouselOrientation === 'vertical'
|
// derived layout genuinely changed, not on every resize tick.
|
||||||
? Math.max((height - scrollBarWidth) / tileSpan, MIN_VISIBLE_TILES)
|
const handleLayoutChange = useCallback((next: CarouselLayoutState) => {
|
||||||
: Math.max((width - scrollBarWidth) / tileSpan, MIN_VISIBLE_TILES)
|
setLayout((prev) =>
|
||||||
|
prev.orientation === next.orientation &&
|
||||||
let maxVisibleTiles = Math.round(tilesThatFit)
|
prev.maxVisibleTiles === next.maxVisibleTiles
|
||||||
if (Math.abs(tilesThatFit - prevTiles) < 0.5) {
|
? prev
|
||||||
maxVisibleTiles = Math.round(prevTiles)
|
: next
|
||||||
} else if (prevTiles !== tilesThatFit) {
|
|
||||||
setPrevTiles(tilesThatFit)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedTiles = useVisualStableUpdate(tracks, maxVisibleTiles)
|
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
|
||||||
if (asideEl.current) {
|
|
||||||
asideEl.current.dataset.lkOrientation = carouselOrientation
|
|
||||||
asideEl.current.style.setProperty(
|
|
||||||
'--lk-max-visible-tiles',
|
|
||||||
maxVisibleTiles.toString()
|
|
||||||
)
|
)
|
||||||
}
|
}, [])
|
||||||
}, [maxVisibleTiles, carouselOrientation])
|
|
||||||
|
const sortedTiles = useVisualStableUpdate(tracks, layout.maxVisibleTiles)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<CarouselLayoutObserver
|
||||||
|
asideEl={asideEl}
|
||||||
|
orientation={orientation}
|
||||||
|
onLayoutChange={handleLayoutChange}
|
||||||
|
/>
|
||||||
|
{/* `key` intentionally remounts the container when orientation flips, */}
|
||||||
|
{/* which resets scroll position and re-runs the observer measurement. */}
|
||||||
<aside
|
<aside
|
||||||
key={carouselOrientation}
|
key={layout.orientation}
|
||||||
className="lk-carousel"
|
className="lk-carousel"
|
||||||
ref={asideEl}
|
ref={asideEl}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<TrackLoop tracks={sortedTiles}>{props.children}</TrackLoop>
|
<TrackLoop tracks={sortedTiles}>{props.children}</TrackLoop>
|
||||||
</aside>
|
</aside>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user