Skip to content

Commit 33f7eef

Browse files
authored
improvement(landing): optimize hero and customer image loading (#7679)
1 parent 568a539 commit 33f7eef

19 files changed

Lines changed: 168 additions & 13 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** Generated by scripts/generate-hero-artwork.ts. */
2+
export const HERO_ARTWORK = {
3+
avifSrcSet:
4+
'/landing/hero-artwork/painting-1080-edb05d03330a.avif 1080w, /landing/hero-artwork/painting-1200-a6f578eae83e.avif 1200w, /landing/hero-artwork/painting-1920-35eadaf0cdee.avif 1920w, /landing/hero-artwork/painting-2048-8538288166d3.avif 2048w, /landing/hero-artwork/painting-3840-d6e7c3f5943d.avif 3840w',
5+
webpSrcSet:
6+
'/landing/hero-artwork/painting-1080-2ccdfef92b36.webp 1080w, /landing/hero-artwork/painting-1200-04e474a8546d.webp 1200w, /landing/hero-artwork/painting-1920-c7a1fa74f312.webp 1920w, /landing/hero-artwork/painting-2048-6a414066a1fd.webp 2048w, /landing/hero-artwork/painting-3840-55734e1694fb.webp 3840w',
7+
src: '/landing/hero-artwork/painting-3840-55734e1694fb.webp',
8+
} as const
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/** @vitest-environment node */
2+
import { renderToStaticMarkup } from 'react-dom/server'
3+
import { describe, expect, it, vi } from 'vitest'
4+
import { HERO_ARTWORK } from '@/app/(landing)/components/hero/components/hero-platform-stage/hero-artwork.generated'
5+
import { HeroPlatformStage } from '@/app/(landing)/components/hero/components/hero-platform-stage/hero-platform-stage'
6+
7+
vi.mock('@sim/emcn', () => ({ cn: (...values: string[]) => values.join(' ') }))
8+
vi.mock('@/app/(landing)/components/hero/components/hero-platform-loop', () => ({
9+
HeroPlatformLoopMount: () => null,
10+
}))
11+
vi.mock(
12+
'@/app/(landing)/components/hero/components/hero-platform-stage/mobile-hero-workflow',
13+
() => ({
14+
MobileHeroWorkflow: () => null,
15+
})
16+
)
17+
18+
describe('HeroPlatformStage loading', () => {
19+
it('preloads only the desktop AVIF selection and keeps the hidden image lazy on mobile', () => {
20+
const html = renderToStaticMarkup(<HeroPlatformStage />)
21+
const preloads = html.match(/<link\b[^>]*rel="preload"[^>]*>/g) ?? []
22+
expect(preloads).toHaveLength(1)
23+
expect(preloads[0]).toContain('media="(min-width: 1024px)"')
24+
expect(preloads[0]).toContain('type="image/avif"')
25+
expect(preloads[0]).toContain(`imageSrcSet="${HERO_ARTWORK.avifSrcSet}"`)
26+
expect(html).toContain(`srcSet="${HERO_ARTWORK.avifSrcSet}"`)
27+
expect(html).toContain(`srcSet="${HERO_ARTWORK.webpSrcSet}"`)
28+
expect(html).toContain('loading="lazy"')
29+
expect(html).not.toContain('/_next/image')
30+
})
31+
})

apps/sim/app/(landing)/components/hero/components/hero-platform-stage/hero-platform-stage.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { cn } from '@sim/emcn'
22
import Image from 'next/image'
33
import { HeroPlatformLoopMount } from '@/app/(landing)/components/hero/components/hero-platform-loop'
4+
import { HERO_ARTWORK } from '@/app/(landing)/components/hero/components/hero-platform-stage/hero-artwork.generated'
45
import { MobileHeroWorkflow } from '@/app/(landing)/components/hero/components/hero-platform-stage/mobile-hero-workflow'
56
import { LANDING_STAGE_RADIUS } from '@/app/(landing)/components/landing-layout'
67

8+
const ARTWORK_SIZES =
9+
'(min-width: 1728px) 1648px, (min-width: 1280px) calc(100vw - 80px), calc(100vw - 72px)'
10+
711
/**
812
* A focused workflow below 1024px; larger screens show the interactive platform
913
* in a painted frame. The window uses a compact gutter until the wide desktop
@@ -12,6 +16,15 @@ import { LANDING_STAGE_RADIUS } from '@/app/(landing)/components/landing-layout'
1216
export function HeroPlatformStage() {
1317
return (
1418
<>
19+
<link
20+
rel='preload'
21+
as='image'
22+
type='image/avif'
23+
media='(min-width: 1024px)'
24+
imageSrcSet={HERO_ARTWORK.avifSrcSet}
25+
imageSizes={ARTWORK_SIZES}
26+
fetchPriority='high'
27+
/>
1528
<MobileHeroWorkflow />
1629
<div
1730
data-preview-stage=''
@@ -21,18 +34,20 @@ export function HeroPlatformStage() {
2134
LANDING_STAGE_RADIUS
2235
)}
2336
>
24-
<div aria-hidden='true' className='pointer-events-none absolute inset-0'>
37+
<picture className='pointer-events-none absolute inset-0'>
38+
<source type='image/avif' srcSet={HERO_ARTWORK.avifSrcSet} sizes={ARTWORK_SIZES} />
39+
<source type='image/webp' srcSet={HERO_ARTWORK.webpSrcSet} sizes={ARTWORK_SIZES} />
2540
<Image
2641
data-preview-background=''
27-
src='/landing/hero-painted-4k.webp'
42+
src={HERO_ARTWORK.src}
2843
alt=''
2944
fill
45+
unoptimized
3046
fetchPriority='high'
31-
quality={90}
32-
sizes='(max-width: 1727px) 100vw, 1648px'
47+
sizes={ARTWORK_SIZES}
3348
className='object-cover dark:brightness-[0.28]'
3449
/>
35-
</div>
50+
</picture>
3651

3752
<div
3853
role='region'

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,23 @@ import { CustomerStoryMedia } from '@/app/(landing)/customers/components/custome
77
interface CustomerStoryCardProps {
88
story: CustomerStory
99
post: ContentMeta
10+
priority?: boolean
11+
sizes?: string
1012
}
1113

1214
/** A quiet editorial link shared by the customer index and next-story section. */
13-
export function CustomerStoryCard({ story, post }: CustomerStoryCardProps) {
15+
export function CustomerStoryCard({
16+
story,
17+
post,
18+
priority = false,
19+
sizes = '(max-width: 767px) calc(100vw - 56px), 640px',
20+
}: CustomerStoryCardProps) {
1421
return (
1522
<Link
1623
href={`/customers/${story.slug}`}
1724
className='group flex min-w-0 flex-col gap-5 rounded-[12px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--text-primary)] focus-visible:outline-offset-4'
1825
>
19-
<CustomerStoryMedia story={story} />
26+
<CustomerStoryMedia story={story} priority={priority} sizes={sizes} />
2027
<div className='flex items-start justify-between gap-6'>
2128
<div>
2229
<p className='mb-2 text-[14px] text-[var(--text-muted)]'>{story.company}</p>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ interface CustomerStoryMediaProps {
55
story: CustomerStory
66
priority?: boolean
77
playable?: boolean
8+
sizes?: string
89
}
910

1011
/** Customer artwork, with on-demand playback on story detail pages. */
1112
export function CustomerStoryMedia({
1213
story,
1314
priority = false,
1415
playable = false,
16+
sizes = '(max-width: 1023px) calc(100vw - 64px), (max-width: 1728px) 78vw, 1374px',
1517
}: CustomerStoryMediaProps) {
1618
return (
1719
<div
@@ -34,7 +36,7 @@ export function CustomerStoryMedia({
3436
src={story.heroImage}
3537
alt={story.heroAlt}
3638
fill
37-
sizes='(max-width: 1023px) calc(100vw - 64px), (max-width: 1728px) 78vw, 1374px'
39+
sizes={sizes}
3840
preload={priority}
3941
quality={90}
4042
className='object-cover'

apps/sim/app/(landing)/customers/page.test.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ vi.mock('@/lib/content/seo', () => ({
2828
}))
2929
vi.mock('@/app/(landing)/components/json-ld/json-ld', () => ({ JsonLd: () => null }))
3030
vi.mock('@/app/(landing)/customers/components/customer-story-card/customer-story-card', () => ({
31-
CustomerStoryCard: ({ story }: { story: CustomerStory }) => (
32-
<a href={`/customers/${story.slug}`}>{story.company}</a>
31+
CustomerStoryCard: ({ story, priority }: { story: CustomerStory; priority?: boolean }) => (
32+
<a href={`/customers/${story.slug}`} data-priority={priority}>
33+
{story.company}
34+
</a>
3335
),
3436
}))
3537
vi.mock('@/app/(landing)/customers/components/customer-story-page/customer-story-page', () => ({
@@ -78,11 +80,21 @@ function setStories(rivianDraft: boolean, expDraft: boolean) {
7880
beforeEach(() => vi.clearAllMocks())
7981

8082
describe('customer publication boundaries', () => {
83+
it('prioritizes only the first rendered card when an earlier story has no content', async () => {
84+
setStories(false, false)
85+
const exp = post('exp-realty', false)
86+
getBySlug.mockImplementation(async (slug: string) => (slug === exp.slug ? exp : null))
87+
const html = renderToStaticMarkup(await CustomersPage())
88+
expect(html).not.toContain('href="/customers/rivian"')
89+
expect(html).toContain('href="/customers/exp-realty" data-priority="true"')
90+
})
91+
8192
it('preserves the complete noindex design preview when every story is a draft', async () => {
8293
setStories(true, true)
8394
const html = renderToStaticMarkup(await CustomersPage())
8495
expect(html).toContain('href="/customers/rivian"')
8596
expect(html).toContain('href="/customers/exp-realty"')
97+
expect(html.match(/data-priority="true"/g)).toHaveLength(1)
8698
expect((await generateMetadata()).robots).toEqual({ index: false, follow: false })
8799
})
88100

apps/sim/app/(landing)/customers/page.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import { CustomerStoryCard } from '@/app/(landing)/customers/components/customer
1313

1414
export const revalidate = 86400
1515

16+
/** Two columns with a 32px gap inside HOME_INSET; below md the cards fill the gutter. */
17+
const CARD_IMAGE_SIZES =
18+
'(min-width: 1728px) 671px, (min-width: 1280px) calc((100vw - 80px) * 5 / 12 - 16px), (min-width: 1024px) calc((100vw - 72px) * 5 / 12 - 16px), (min-width: 768px) calc(50vw - 48px), calc(100vw - 56px)'
19+
1620
export async function generateMetadata(): Promise<Metadata> {
1721
const published = await getAllCustomerStoryMeta()
1822
return {
@@ -52,9 +56,19 @@ export default async function Page() {
5256
<h2 id='customer-stories-heading' className='sr-only'>
5357
Featured customer stories
5458
</h2>
55-
{stories.map(({ story, post }) =>
56-
post ? <CustomerStoryCard key={story.slug} story={story} post={post} /> : null
57-
)}
59+
{stories
60+
.filter(({ post }) => post)
61+
.map(({ story, post }, index) =>
62+
post ? (
63+
<CustomerStoryCard
64+
key={story.slug}
65+
story={story}
66+
post={post}
67+
priority={index === 0}
68+
sizes={CARD_IMAGE_SIZES}
69+
/>
70+
) : null
71+
)}
5872
</section>
5973
</div>
6074
</main>

apps/sim/next.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ const nextConfig: NextConfig = {
273273
source: '/landing/footer-artwork/:path*',
274274
headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
275275
},
276+
{
277+
/** Generated hero artwork uses content hashes, so URLs are immutable. */
278+
source: '/landing/hero-artwork/:path*',
279+
headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
280+
},
276281
{
277282
source: '/.well-known/:path*',
278283
headers: [
60.6 KB
Loading
Binary file not shown.

0 commit comments

Comments
 (0)