Skip to content

Commit 93382be

Browse files
committed
perf: defer application starter on landing pages
1 parent 8bcbbc5 commit 93382be

File tree

5 files changed

+180
-14
lines changed

5 files changed

+180
-14
lines changed

src/components/ApplicationStarter.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import type {
1818
ApplicationStarterContext,
1919
ApplicationStarterResult,
2020
} from '~/utils/application-starter'
21-
import { DeployDialog } from '~/components/builder/DeployDialog'
2221
import {
2322
Collapsible,
2423
CollapsibleContent,
@@ -41,7 +40,7 @@ import { useApplicationBuilder } from '~/components/application-builder/useAppli
4140
import { Button, GitHub } from '~/ui'
4241
import { trackPostHogEvent } from '~/utils/posthog'
4342

44-
interface ApplicationStarterProps {
43+
export interface ApplicationStarterProps {
4544
alwaysShowPostAnalysisSection?: boolean
4645
builderIntegration?: ApplicationStarterBuilderIntegration
4746
className?: string
@@ -71,6 +70,12 @@ const LazyApplicationStarterHotkeys = React.lazy(() =>
7170
})),
7271
)
7372

73+
const LazyDeployDialog = React.lazy(() =>
74+
import('~/components/builder/DeployDialog').then((m) => ({
75+
default: m.DeployDialog,
76+
})),
77+
)
78+
7479
const starterPackageManagers = ['pnpm', 'npm', 'yarn', 'bun'] as const
7580
const starterToolchains = ['biome', 'eslint'] as const
7681

@@ -238,12 +243,16 @@ export function ApplicationStarter({
238243
</ClientOnly>
239244
) : null}
240245

241-
<DeployDialog
242-
isOpen={isDeployDialogOpen}
243-
onClose={() => setIsDeployDialogOpen(false)}
244-
provider={deployDialogProvider}
245-
starterRecipe={result?.recipe ?? null}
246-
/>
246+
{isDeployDialogOpen ? (
247+
<React.Suspense fallback={null}>
248+
<LazyDeployDialog
249+
isOpen={isDeployDialogOpen}
250+
onClose={() => setIsDeployDialogOpen(false)}
251+
provider={deployDialogProvider}
252+
starterRecipe={result?.recipe ?? null}
253+
/>
254+
</React.Suspense>
255+
) : null}
247256

248257
<div className="relative">
249258
{compact ? (
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import * as React from 'react'
2+
3+
import type { ApplicationStarterProps } from '~/components/ApplicationStarter'
4+
5+
const LazyApplicationStarter = React.lazy(() =>
6+
import('~/components/ApplicationStarter').then((m) => ({
7+
default: m.ApplicationStarter,
8+
})),
9+
)
10+
11+
export function DeferredApplicationStarter(props: ApplicationStarterProps) {
12+
const wrapperRef = React.useRef<HTMLDivElement | null>(null)
13+
const [shouldLoad, setShouldLoad] = React.useState(false)
14+
15+
React.useEffect(() => {
16+
if (shouldLoad) {
17+
return
18+
}
19+
20+
const element = wrapperRef.current
21+
22+
if (!element || typeof IntersectionObserver === 'undefined') {
23+
setShouldLoad(true)
24+
return
25+
}
26+
27+
const observer = new IntersectionObserver(
28+
(entries) => {
29+
if (!entries.some((entry) => entry.isIntersecting)) {
30+
return
31+
}
32+
33+
setShouldLoad(true)
34+
observer.disconnect()
35+
},
36+
{ rootMargin: '320px 0px' },
37+
)
38+
39+
observer.observe(element)
40+
41+
return () => {
42+
observer.disconnect()
43+
}
44+
}, [shouldLoad])
45+
46+
React.useEffect(() => {
47+
if (shouldLoad || typeof window === 'undefined') {
48+
return
49+
}
50+
51+
const requestIdleCallback = window.requestIdleCallback
52+
const cancelIdleCallback = window.cancelIdleCallback
53+
54+
if (
55+
typeof requestIdleCallback === 'function' &&
56+
typeof cancelIdleCallback === 'function'
57+
) {
58+
const idleId = requestIdleCallback(
59+
() => {
60+
setShouldLoad(true)
61+
},
62+
{ timeout: 2500 },
63+
)
64+
65+
return () => {
66+
cancelIdleCallback(idleId)
67+
}
68+
}
69+
70+
const timeoutId = window.setTimeout(() => {
71+
setShouldLoad(true)
72+
}, 1500)
73+
74+
return () => {
75+
window.clearTimeout(timeoutId)
76+
}
77+
}, [shouldLoad])
78+
79+
return (
80+
<div ref={wrapperRef}>
81+
{shouldLoad ? (
82+
<React.Suspense
83+
fallback={<DeferredApplicationStarterFallback mode={props.mode} />}
84+
>
85+
<LazyApplicationStarter {...props} />
86+
</React.Suspense>
87+
) : (
88+
<DeferredApplicationStarterFallback mode={props.mode} />
89+
)}
90+
</div>
91+
)
92+
}
93+
94+
function DeferredApplicationStarterFallback({
95+
mode = 'full',
96+
}: {
97+
mode?: ApplicationStarterProps['mode']
98+
}) {
99+
if (mode === 'compact') {
100+
return (
101+
<div aria-hidden="true" className="space-y-3">
102+
<div className="h-5 w-48 rounded bg-gray-200 dark:bg-gray-800" />
103+
<div className="overflow-hidden rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-950">
104+
<div className="border-b border-gray-200 px-3 py-2 dark:border-gray-800">
105+
<div className="h-3 w-10 rounded bg-gray-200 dark:bg-gray-800" />
106+
<div className="mt-2 flex flex-wrap gap-2">
107+
<div className="h-8 w-24 rounded-md bg-gray-200 dark:bg-gray-800" />
108+
<div className="h-8 w-28 rounded-md bg-gray-200 dark:bg-gray-800" />
109+
<div className="h-8 w-20 rounded-md bg-gray-200 dark:bg-gray-800" />
110+
</div>
111+
</div>
112+
113+
<div className="px-3 pb-2 pt-2">
114+
<div className="h-3 w-12 rounded bg-gray-200 dark:bg-gray-800" />
115+
<div className="mt-2 h-20 rounded-md bg-gray-100 dark:bg-gray-900" />
116+
</div>
117+
118+
<div className="border-t border-gray-200 px-3 py-2 dark:border-gray-800">
119+
<div className="h-8 w-24 rounded-md bg-gray-200 dark:bg-gray-800" />
120+
</div>
121+
</div>
122+
</div>
123+
)
124+
}
125+
126+
return (
127+
<div
128+
aria-hidden="true"
129+
className="overflow-hidden rounded-[1rem] border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-950"
130+
>
131+
<div className="border-b border-gray-200 bg-gray-50/70 px-5 py-4 dark:border-gray-800 dark:bg-gray-900/50">
132+
<div className="h-7 w-64 max-w-full rounded bg-gray-200 dark:bg-gray-800" />
133+
<div className="mt-4 flex flex-wrap gap-2">
134+
<div className="h-9 w-28 rounded-lg bg-gray-200 dark:bg-gray-800" />
135+
<div className="h-9 w-32 rounded-lg bg-gray-200 dark:bg-gray-800" />
136+
<div className="h-9 w-24 rounded-lg bg-gray-200 dark:bg-gray-800" />
137+
</div>
138+
</div>
139+
140+
<div className="relative border-b border-gray-200 dark:border-gray-800">
141+
<div className="px-5 pt-4">
142+
<div className="h-3 w-12 rounded bg-gray-200 dark:bg-gray-800" />
143+
</div>
144+
<div className="px-5 pb-4 pt-3">
145+
<div className="h-28 rounded-xl bg-gray-100 dark:bg-gray-900" />
146+
</div>
147+
148+
<div className="border-t border-gray-200 px-5 py-4 dark:border-gray-800">
149+
<div className="flex flex-wrap gap-3">
150+
<div className="h-10 w-28 rounded-lg bg-gray-200 dark:bg-gray-800" />
151+
<div className="h-10 w-36 rounded-lg bg-gray-200 dark:bg-gray-800" />
152+
</div>
153+
</div>
154+
</div>
155+
</div>
156+
)
157+
}

src/components/landing/RouterLanding.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { LibraryStatsSection } from '~/components/LibraryStatsSection'
1414
import { LazyCodeExampleCard } from '~/components/LazyCodeExampleCard'
1515
import { LazyLandingCommunitySection } from '~/components/LazyLandingCommunitySection'
1616
import { StackBlitzSection } from '~/components/StackBlitzSection'
17-
import { ApplicationStarter } from '~/components/ApplicationStarter'
17+
import { DeferredApplicationStarter } from '~/components/DeferredApplicationStarter'
1818

1919
const library = getLibrary('router')
2020

@@ -78,7 +78,7 @@ export default function RouterLanding() {
7878
<div className="space-y-6">
7979
<div className="mx-auto w-full max-w-[1021px] px-4 pt-4 sm:px-6">
8080
<div className="mx-auto">
81-
<ApplicationStarter
81+
<DeferredApplicationStarter
8282
context="router"
8383
forceRouterOnly
8484
secondaryActionLabel="Build Router App on Netlify"

src/components/landing/StartLanding.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { BrandXIcon } from '~/components/icons/BrandXIcon'
1515
import { LibraryPageContainer } from '~/components/LibraryPageContainer'
1616
import { LibraryStatsSection } from '~/components/LibraryStatsSection'
1717
import { LazyLandingCommunitySection } from '~/components/LazyLandingCommunitySection'
18-
import { ApplicationStarter } from '~/components/ApplicationStarter'
18+
import { DeferredApplicationStarter } from '~/components/DeferredApplicationStarter'
1919

2020
const library = getLibrary('start')
2121

@@ -42,7 +42,7 @@ export default function StartLanding() {
4242
<div className="space-y-6">
4343
<div className="mx-auto w-full max-w-[1021px] px-4 pt-4 sm:px-6">
4444
<div className="mx-auto">
45-
<ApplicationStarter
45+
<DeferredApplicationStarter
4646
context="start"
4747
secondaryActionLabel="Build Start on Netlify"
4848
title="What would you like to build with TanStack Start?"

src/routes/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { Card } from '~/components/Card'
2727
import LibraryCard from '~/components/LibraryCard'
2828
import { FeaturedShowcases } from '~/components/ShowcaseSection'
2929
import { Button } from '~/ui'
30-
import { ApplicationStarter } from '~/components/ApplicationStarter'
30+
import { DeferredApplicationStarter } from '~/components/DeferredApplicationStarter'
3131
import { seo } from '~/utils/seo'
3232

3333
const LazyBrandContextMenu = React.lazy(() =>
@@ -226,7 +226,7 @@ function Index() {
226226
</div>
227227
</div>
228228
<div className="mx-auto mt-16 w-full max-w-[1021px] px-4 sm:px-6 md:mt-20 lg:mt-14 xl:mt-12">
229-
<ApplicationStarter
229+
<DeferredApplicationStarter
230230
context="home"
231231
enableHotkeys
232232
primaryButtonColor="cyan"

0 commit comments

Comments
 (0)