Skip to content

Commit be9d594

Browse files
committed
feat: lazy loading huge main page components
1 parent 3833dd9 commit be9d594

File tree

23 files changed

+334
-77
lines changed

23 files changed

+334
-77
lines changed

package-lock.json

Lines changed: 0 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/blocks/Contributors/Contributors.tsx

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,25 @@
1-
import {Animatable, AnimateBlock, HTML} from '@gravity-ui/page-constructor';
1+
import {AnimateBlock, HTML} from '@gravity-ui/page-constructor';
22
import {Button} from '@gravity-ui/uikit';
33
import React from 'react';
4+
import {LazyExpandableContributorsList} from 'src/components/ExpandableContributorList';
45

5-
import {Contributor} from '../../api';
6-
import {ExpandableContributorList} from '../../components/ExpandableContributorList';
76
import {block} from '../../utils';
8-
import {CustomBlock} from '../constants';
97

108
import './Contributors.scss';
9+
import {ContributorsProps} from './types';
1110

1211
const b = block('contributors');
1312

14-
type TelegramLink = {
15-
title: string;
16-
href: string;
17-
};
18-
19-
export type ContributorsProps = Animatable & {
20-
title: string;
21-
link: TelegramLink;
22-
contributors: Contributor[];
23-
};
24-
25-
export type ContributorsModel = ContributorsProps & {
26-
type: CustomBlock.Contributors;
27-
};
13+
export const ContributorsBlock: React.FC<ContributorsProps> = ({animated, title, link}) => {
14+
const [contributorsAmount, setContributorsAmount] = React.useState('');
2815

29-
export const ContributorsBlock: React.FC<ContributorsProps> = ({
30-
animated,
31-
title,
32-
link,
33-
contributors,
34-
}) => {
3516
return (
3617
<AnimateBlock className={b()} animate={animated}>
3718
<div className={b('header-wrapper')}>
3819
<h2 className={b('header-title')}>
3920
<HTML>{title}</HTML>
4021
</h2>
41-
<div className={b('header-count')}>{contributors.length}</div>
22+
<div className={b('header-count')}>{contributorsAmount}</div>
4223
<div>
4324
<Button
4425
size="xl"
@@ -53,7 +34,11 @@ export const ContributorsBlock: React.FC<ContributorsProps> = ({
5334
</div>
5435

5536
<section className={b('section')}>
56-
<ExpandableContributorList contributors={contributors} />
37+
<LazyExpandableContributorsList
38+
onLoad={(_, props) => {
39+
setContributorsAmount(String(props.contributors.length) || '0');
40+
}}
41+
/>
5742
</section>
5843
</AnimateBlock>
5944
);

src/blocks/Contributors/types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {Animatable} from '@gravity-ui/page-constructor';
2+
import {Contributor} from 'src/api';
3+
4+
import {CustomBlock} from '../constants';
5+
6+
type TelegramLink = {
7+
title: string;
8+
href: string;
9+
};
10+
11+
export type ContributorsProps = Animatable & {
12+
title: string;
13+
link: TelegramLink;
14+
contributors: Contributor[];
15+
};
16+
17+
export type ContributorsModel = ContributorsProps & {
18+
type: CustomBlock.Contributors;
19+
};

src/blocks/UISamples/samples.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import {useTranslation} from 'next-i18next';
22
import {useMemo} from 'react';
33
import {
4-
ApartmentCardPreview,
5-
DashboardPreview2,
6-
KubernetesPreview,
7-
MailPreview,
8-
OsnPreview,
9-
TablePreview,
10-
TasksPreview,
4+
LazyApartmentCardPreview,
5+
LazyDashboardPreview2,
6+
LazyKubernetesPreview,
7+
LazyMailPreview,
8+
LazyOsnPreview,
9+
LazyTablePreview,
10+
LazyTasksPreview,
1111
} from 'src/components/UISamples';
1212

1313
import dashboardImage from '../../assets/ui-samples/card-dashboard.jpg';
@@ -36,50 +36,50 @@ export const useSampleComponents = () => {
3636
{
3737
type: SampleComponent.Dashboard,
3838
imagePreviewSrc: dashboardImage.src,
39-
Component: DashboardPreview2,
39+
Component: LazyDashboardPreview2,
4040
title: t('ui_samples_dashboard_tab'),
4141
breadCrumbsItems: ['Dashboard'],
4242
blank: true,
4343
},
4444
{
4545
type: SampleComponent.HotelBooking,
4646
imagePreviewSrc: hotelBookingImage.src,
47-
Component: ApartmentCardPreview,
47+
Component: LazyApartmentCardPreview,
4848
title: t('ui_samples_apartment_tab'),
4949
blank: true,
5050
},
5151
{
5252
type: SampleComponent.Listing,
5353
imagePreviewSrc: listingImage.src,
54-
Component: TablePreview,
54+
Component: LazyTablePreview,
5555
title: t('ui_samples_table_tab'),
5656
breadCrumbsItems: ['Table'],
5757
},
5858
{
5959
type: SampleComponent.TaskTracker,
6060
imagePreviewSrc: taskTrackerImage.src,
61-
Component: TasksPreview,
61+
Component: LazyTasksPreview,
6262
title: t('ui_samples_task_tracker_tab'),
6363
blank: true,
6464
},
6565
{
6666
type: SampleComponent.Kubernetes,
6767
imagePreviewSrc: kubernetesImage.src,
68-
Component: KubernetesPreview,
68+
Component: LazyKubernetesPreview,
6969
title: t('ui_samples_kubernetes_tab'),
7070
blank: true,
7171
},
7272
{
7373
type: SampleComponent.Osn,
7474
imagePreviewSrc: osnImage.src,
75-
Component: OsnPreview,
75+
Component: LazyOsnPreview,
7676
title: t('ui_samples_osn_tab'),
7777
blank: true,
7878
},
7979
{
8080
type: SampleComponent.Mail,
8181
imagePreviewSrc: mailImage.src,
82-
Component: MailPreview,
82+
Component: LazyMailPreview,
8383
title: t('ui_samples_mail_tab'),
8484
blank: true,
8585
},

src/components/ExpandableContributorList/ExpandableContributorList.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ $block: '.#{variables.$ns}expandable-contributor-list';
1919
grid-template-rows: unset;
2020
}
2121

22+
&__loader {
23+
width: 100%;
24+
height: 276px;
25+
display: flex;
26+
justify-content: center;
27+
align-items: center;
28+
}
29+
2230
&__inset-shadow {
2331
position: absolute;
2432
bottom: 0;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {Loader} from '@gravity-ui/uikit';
2+
import {Api} from 'src/api';
3+
import {block} from 'src/utils';
4+
5+
import {IntersectionLoadComponent} from '../IntersectionLoadComponent/IntersectionLoadComponent';
6+
7+
type Props = Omit<
8+
React.ComponentProps<
9+
typeof IntersectionLoadComponent<Awaited<ReturnType<typeof getComponent>>>
10+
>,
11+
'cacheKey' | 'getComponent' | 'getComponentProps' | 'loader'
12+
>;
13+
14+
const b = block('expandable-contributor-list');
15+
16+
const getComponent = async () => {
17+
return (await import('./ExpandableContributorList')).ExpandableContributorList;
18+
};
19+
20+
const getComponentProps = async () => {
21+
const contributors = await Api.instance.fetchAllContributorsWithCache();
22+
23+
return {contributors};
24+
};
25+
26+
export const LazyExpandableContributorsList: React.FC<Props> = (props) => {
27+
return (
28+
<IntersectionLoadComponent
29+
cacheKey="ExpandableContributorList"
30+
getComponent={getComponent}
31+
getComponentProps={getComponentProps}
32+
loader={<Loader size="l" className={b('loader')} />}
33+
{...props}
34+
/>
35+
);
36+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './ExpandableContributorList';
2+
export {LazyExpandableContributorsList} from './LazyExpandableContributorsList';
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use client';
2+
3+
import {useIntersection} from '@gravity-ui/uikit';
4+
import React from 'react';
5+
6+
type Props<Component extends React.ComponentType> = {
7+
cacheKey: string;
8+
getComponent: () => Promise<Component>;
9+
getComponentProps?: NoInfer<() => Promise<React.ComponentProps<Component>>>;
10+
loader: React.ReactNode;
11+
intersectionOptions?: IntersectionObserverInit;
12+
onIntersect?: () => void;
13+
onLoad?: NoInfer<(component: Component, props: React.ComponentProps<Component>) => void>;
14+
};
15+
16+
const cache = new Map();
17+
18+
/**
19+
* HOC for lazy loading component when it reaches viewport
20+
* @param {string} cacheKey
21+
* @param {function(): Promise<Component>} getComponent component import function
22+
* @param {function(): Promise<React.ComponentProps<Component>>} getComponentProps async function for component props; async for loading data from server by demand
23+
* @param {IntersectionObserverInit} intersectionOptions IntersectionObserver options
24+
* @param {JSX.Element} loader displaying while component is being loaded
25+
* @param {function(): void} onIntersect callback, fires when component reaches viewport
26+
* @param {function(Component, React.ComponentProps<Component>): void} onLoad callback, fires when component is loaded
27+
* @returns {JSX.Element} loaded component or loader
28+
*/
29+
// unknown/{}/object doesn't work
30+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31+
export const IntersectionLoadComponent = <Component extends React.ComponentType<any>>({
32+
cacheKey,
33+
getComponent,
34+
getComponentProps = () => Promise.resolve({} as React.ComponentProps<Component>),
35+
loader,
36+
intersectionOptions,
37+
onIntersect,
38+
onLoad,
39+
}: Props<Component>) => {
40+
const [waitingLoad, setWaitingLoad] = React.useState(!cache.has(cacheKey));
41+
const [intersectionElementRef, setIntersectionElementRef] =
42+
React.useState<HTMLDivElement | null>(null);
43+
44+
const getComponentWithPropsCached = React.useCallback(async () => {
45+
if (cache.has(cacheKey)) {
46+
return cache.get(cacheKey);
47+
}
48+
49+
const [Component, props] = await Promise.all([getComponent(), getComponentProps()]);
50+
51+
cache.set(cacheKey, {Component, props});
52+
53+
return {Component, props};
54+
}, [getComponent]);
55+
56+
const LazyComponent = React.lazy(async () => {
57+
const {Component, props} = await getComponentWithPropsCached();
58+
59+
onLoad?.(Component, props);
60+
61+
return {default: () => <Component key={cacheKey} {...props} />};
62+
});
63+
64+
useIntersection({
65+
element: intersectionElementRef,
66+
onIntersect: () => {
67+
setWaitingLoad(false);
68+
onIntersect?.();
69+
},
70+
options: intersectionOptions,
71+
});
72+
73+
if (cache.has(cacheKey)) {
74+
const {Component, props} = cache.get(cacheKey);
75+
76+
return <Component key={cacheKey} {...props} />;
77+
}
78+
79+
if (waitingLoad) {
80+
return (
81+
<div key={cacheKey}>
82+
<div ref={setIntersectionElementRef} />
83+
{loader}
84+
</div>
85+
);
86+
}
87+
88+
return (
89+
<div key={cacheKey}>
90+
<React.Suspense fallback={loader}>
91+
<LazyComponent />
92+
</React.Suspense>
93+
</div>
94+
);
95+
};
96+
97+
IntersectionLoadComponent.clearCache = (key?: string) => (key ? cache.delete(key) : cache.clear());

src/components/Landing/Landing.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import {PageConstructor, PageContent} from '@gravity-ui/page-constructor';
22
import {useTranslation} from 'next-i18next';
33
import {useRouter} from 'next/router';
44
import React from 'react';
5+
import {ContributorsBlock} from 'src/blocks/Contributors/Contributors';
56
import Examples from 'src/blocks/Examples/Examples';
67
import {UISamplesBlock} from 'src/blocks/UISamples/UISamples';
78
import {CustomPageContent} from 'src/content/types';
89

9-
import type {Contributor, LibWithMetadata} from '../../api';
10-
import {ContributorsBlock} from '../../blocks/Contributors/Contributors';
10+
import type {LibWithMetadata} from '../../api';
1111
import {CustomHeader} from '../../blocks/CustomHeader/CustomHeader';
1212
import {GithubStarsBlock} from '../../blocks/GithubStarsBlock/GithubStarsBlock';
1313
import {IFrameBlock} from '../../blocks/IFrameBlock/IFrameBlock';
@@ -41,11 +41,10 @@ const filterBlocks = ({blocks, ...rest}: CustomPageContent): CustomPageContent =
4141

4242
type Props = {
4343
libs: LibWithMetadata[];
44-
contributors: Contributor[];
4544
backgroundImageSrc: string;
4645
};
4746

48-
export const Landing: React.FC<Props> = ({libs, contributors, backgroundImageSrc}) => {
47+
export const Landing: React.FC<Props> = ({libs, backgroundImageSrc}) => {
4948
const {t} = useTranslation();
5049
const {pathname} = useRouter();
5150

@@ -58,8 +57,8 @@ export const Landing: React.FC<Props> = ({libs, contributors, backgroundImageSrc
5857
content={
5958
filterBlocks(
6059
pathname === '/rtl'
61-
? getRtlLanding({t, libs, contributors, backgroundImageSrc})
62-
: getLanding({t, libs, contributors, backgroundImageSrc}),
60+
? getRtlLanding({t, libs, backgroundImageSrc})
61+
: getLanding({t, libs, backgroundImageSrc}),
6362
) as PageContent
6463
}
6564
custom={{

src/components/Layout/Layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const Layout: React.FC<LayoutProps> = ({
7272
};
7373
}, [isRtl]);
7474

75-
const pageConent = (
75+
const pageContent = (
7676
<div className={b()}>
7777
{!showOnlyContent && (
7878
<div className={b('menu')} id={MENU_ID}>
@@ -101,10 +101,10 @@ export const Layout: React.FC<LayoutProps> = ({
101101
<React.Fragment>
102102
{isPageConstructor ? (
103103
<PageConstructorProvider theme={DEFAULT_THEME as PageConstructorTheme}>
104-
{pageConent}
104+
{pageContent}
105105
</PageConstructorProvider>
106106
) : (
107-
pageConent
107+
pageContent
108108
)}
109109
</React.Fragment>
110110
</ThemeProvider>

0 commit comments

Comments
 (0)