Skip to content

Commit 23afa2a

Browse files
authored
fix(landing): optimize customer media loading (#7656)
1 parent 0bba808 commit 23afa2a

6 files changed

Lines changed: 70 additions & 10 deletions

File tree

apps/sim/app/(landing)/components/featured-customer/featured-customer-card.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,16 @@ interface FeaturedCustomerCardProps {
3434
story: FeaturedCustomerStory
3535
active: boolean
3636
emphasized: boolean
37+
preload: boolean
3738
}
3839

3940
/** Shared media and editorial caption treatment for each featured-customer slide. */
40-
export function FeaturedCustomerCard({ story, active, emphasized }: FeaturedCustomerCardProps) {
41+
export function FeaturedCustomerCard({
42+
story,
43+
active,
44+
emphasized,
45+
preload,
46+
}: FeaturedCustomerCardProps) {
4147
const videoRef = useRef<HTMLVideoElement>(null)
4248
const reducedMotion = usePrefersReducedMotion()
4349
const isFilm = story.media.kind === 'video'
@@ -51,10 +57,9 @@ export function FeaturedCustomerCard({ story, active, emphasized }: FeaturedCust
5157
}
5258

5359
/**
54-
* The film streams only while the slide is on screen in a visible tab:
55-
* `play()` defeats `preload='none'`, so calling it at mount would pull
56-
* the whole file for a section well below the fold. Autoplay may still be
57-
* blocked, in which case the poster remains the fallback.
60+
* Preload prepares neighboring films when the carousel reaches the viewport.
61+
* Playback is restricted to the active, visible slide in a visible tab.
62+
* If autoplay is blocked, the poster remains the fallback.
5863
*/
5964
const canObserve = typeof IntersectionObserver !== 'undefined'
6065
let inView = !canObserve
@@ -103,7 +108,7 @@ export function FeaturedCustomerCard({ story, active, emphasized }: FeaturedCust
103108
loop
104109
muted
105110
playsInline
106-
preload='none'
111+
preload={preload && !reducedMotion ? 'auto' : 'none'}
107112
src={story.media.src}
108113
tabIndex={-1}
109114
className='pointer-events-none absolute inset-0 size-full rounded-[inherit] object-cover motion-reduce:hidden'

apps/sim/app/(landing)/components/featured-customer/featured-customer.test.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ beforeEach(() => {
4343
afterEach(() => {
4444
vi.useRealTimers()
4545
vi.restoreAllMocks()
46+
vi.unstubAllGlobals()
4647
document.body.replaceChildren()
4748
})
4849

@@ -87,7 +88,7 @@ describe('FeaturedCustomer', () => {
8788
)
8889
const expVideo = expRealty.querySelector('video') as HTMLVideoElement
8990
expect(expVideo.getAttribute('src')).toBe('/landing/customer-stories/exp-house-color-loop.mp4')
90-
expect(expVideo.getAttribute('preload')).toBe('none')
91+
expect(expVideo.getAttribute('preload')).toBe('auto')
9192
expect(expVideo.muted).toBe(true)
9293
expect(vi.mocked(video.play).mock.contexts).not.toContain(expVideo)
9394
expect(
@@ -178,6 +179,55 @@ describe('FeaturedCustomer', () => {
178179
})
179180
})
180181

182+
it('prepares neighboring videos only once the carousel is visible without starting playback', () => {
183+
const observers: Array<{
184+
target: Element
185+
intersect: (isIntersecting: boolean) => void
186+
}> = []
187+
vi.stubGlobal(
188+
'IntersectionObserver',
189+
class {
190+
constructor(private readonly callback: IntersectionObserverCallback) {}
191+
192+
observe(target: Element) {
193+
observers.push({
194+
target,
195+
intersect: (isIntersecting) =>
196+
this.callback(
197+
[{ target, isIntersecting } as IntersectionObserverEntry],
198+
{} as IntersectionObserver
199+
),
200+
})
201+
}
202+
203+
disconnect() {}
204+
}
205+
)
206+
const host = document.createElement('div')
207+
document.body.appendChild(host)
208+
const root = createRoot(host)
209+
act(() => root.render(<FeaturedCustomer />))
210+
211+
const videos = [...host.querySelectorAll('video')]
212+
expect(videos.map((video) => video.preload)).toEqual(['none', 'none'])
213+
expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled()
214+
215+
const regionObserver = observers.find(({ target }) => target.tagName === 'DIV')!
216+
act(() => regionObserver.intersect(true))
217+
expect(videos.map((video) => video.preload)).toEqual(['auto', 'auto'])
218+
expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled()
219+
220+
const playbackObserver = observers.find(({ target }) => target === videos[0])!
221+
act(() => playbackObserver.intersect(true))
222+
expect(vi.mocked(HTMLMediaElement.prototype.play).mock.contexts).toEqual([videos[0]])
223+
224+
motionPreference.reduced = true
225+
act(() => root.render(<FeaturedCustomer />))
226+
expect(videos.map((video) => video.preload)).toEqual(['none', 'none'])
227+
228+
act(() => root.unmount())
229+
})
230+
181231
it('keeps video paused with reduced motion and responds to preference changes', () => {
182232
motionPreference.reduced = true
183233
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true

apps/sim/app/(landing)/components/featured-customer/featured-customer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
LANDING_GUTTER,
1313
LANDING_STAGE_RADIUS,
1414
} from '@/app/(landing)/components/landing-layout'
15+
import { useLazyMount } from '@/app/(landing)/hooks/use-lazy-mount'
1516

1617
const WHEEL_GESTURE_GAP_MS = 200
1718
const WHEEL_THRESHOLD_PX = 50
@@ -58,6 +59,7 @@ const CUSTOMER_STORIES: FeaturedCustomerStory[] = [
5859
export function FeaturedCustomer() {
5960
const railRef = useRef<HTMLDivElement>(null)
6061
const touchStartRef = useRef<{ id: number; x: number; y: number } | null>(null)
62+
const { ref: mediaRegionRef, inView: preloadVideos } = useLazyMount('0px')
6163
const [activeIndex, setActiveIndex] = useState(0)
6264
const [previewedIndex, setPreviewedIndex] = useState<number | null>(null)
6365
const activeStory = CUSTOMER_STORIES[activeIndex]
@@ -143,7 +145,7 @@ export function FeaturedCustomer() {
143145
aria-roledescription='carousel'
144146
className='w-full overflow-hidden'
145147
>
146-
<div className={cn(LANDING_CONTENT_WIDTH, LANDING_GUTTER)}>
148+
<div ref={mediaRegionRef} className={cn(LANDING_CONTENT_WIDTH, LANDING_GUTTER)}>
147149
<div className='mb-4 flex items-center justify-end gap-2 xl:pr-24'>
148150
<FeaturedCustomerNavigationButton
149151
direction='previous'
@@ -210,6 +212,7 @@ export function FeaturedCustomer() {
210212
story={story}
211213
active={isActive}
212214
emphasized={isActive || isPreviewed}
215+
preload={preloadVideos}
213216
/>
214217
{!isActive && (
215218
<button

apps/sim/app/(landing)/customers/components/customer-story-media/customer-story-media.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ describe('CustomerStoryMedia', () => {
2727
const html = renderToStaticMarkup(<CustomerStoryMedia story={exp} />)
2828

2929
expect(html).not.toContain('<video')
30-
expect(html).toContain(exp.logo.src)
30+
expect(html).toContain(encodeURIComponent(exp.heroImage!))
31+
expect(html).toContain(exp.heroAlt)
3132
})
3233

3334
it('keeps the artwork fallback for stories without video', () => {

apps/sim/lib/customers/data.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export const CUSTOMER_STORIES: readonly CustomerStory[] = [
3131
company: 'eXp Realty',
3232
industry: 'Real estate',
3333
focus: ['Real estate operations', 'AI workflows', 'Governance'],
34-
heroAlt: 'eXp Realty',
34+
heroImage: '/landing/customers/exp-beach-house.jpg',
35+
heroAlt: 'A modern beach house overlooking the ocean',
3536
heroVideo: {
3637
src: '/landing/customer-stories/exp-house-color-loop.mp4',
3738
poster: '/landing/customer-stories/exp-house-color-poster.jpg',
Binary file not shown.

0 commit comments

Comments
 (0)