Skip to content

Commit 9109ed3

Browse files
committed
chore(activityCatalog): optimize page size
This PR refactors the data loading of the activity catalog to optimize its page size. Currently, the activity catalog is 3mb uncompressed (431kb compressed) which severely degrades performance and incurs bandwidth costs. \## Background The activity catalog uses a full-text search database called Orama. To eliminate the need to fetch data asynchronously, the original implementation serialized the Orama database to json on the server side and passed back to the client which deserializes it. Additionally, the server pre-rendered all 150+ activities and added it to the HTML response. The net effect is that all activity renders, the serialized database (which contains the same activities) were added to the payload. \## Optimization To optimize this, only the first 8 activities (the first two rows on the largest screen size) will be serialized (instead of all 100+ activities). The client will be responsible for loading the database asynchronously and populating the remaining activities. This optimization effectively allows the perceived performance to be high by pre-rendering only content above the fold. The remaining activities will be rendered in asynchronously after the page has loaded. \## Perforamnce Gains In testing, the current page is approximately 3mb uncompresed and 431kb compressed. It has a lighthouse performance score of 58. After these changes, the page size is 972kb uncompressed and 120kb compressed (a 3x improvement). It has a lighthouse performance score of 72.
1 parent 51df895 commit 9109ed3

File tree

5 files changed

+105
-81
lines changed

5 files changed

+105
-81
lines changed

apps/marketing/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
"@opentelemetry/sdk-logs": "^0.208.0",
6060
"@opentelemetry/sdk-metrics": "^2.2.0",
6161
"@orama/orama": "^3.1.14",
62-
"@orama/plugin-data-persistence": "^3.1.14",
6362
"@statsig/js-client": "^3.30.0",
6463
"@statsig/react-bindings": "^3.30.0",
6564
"@statsig/web-analytics": "^3.30.0",

apps/marketing/src/app/[brand]/[locale]/activities/[activityType]/page.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {Results, search} from '@orama/orama';
2-
import {persist} from '@orama/plugin-data-persistence';
32
import {Metadata} from 'next';
43
import {notFound} from 'next/navigation';
54
import {Suspense} from 'react';
@@ -90,20 +89,15 @@ export default async function ActivitiesPage({
9089
}
9190

9291
// Fetch activities from Contentful
93-
const contentfulActivities = await getContentfulActivities(activityType);
92+
const contentfulActivities = (await getContentfulActivities(
93+
activityType,
94+
)) as unknown as Entry<Activity>[];
9495

9596
// Create Orama database from Contentful activities
9697
const db = createDatabase(
9798
contentfulActivities as unknown as Entry<Activity>[],
9899
);
99100

100-
/**
101-
* Serializes the Orama database for client-side use
102-
*/
103-
const getSerializedOramaDatabase = async () => {
104-
return await persist(db, 'json');
105-
};
106-
107101
/**
108102
* Finds all unique values for each facet in the Orama database.
109103
*/
@@ -132,7 +126,7 @@ export default async function ActivitiesPage({
132126
const getAllActivities = async () => {
133127
const allActivityResults = await search(db, {
134128
term: '',
135-
limit: 200,
129+
limit: 8,
136130
sortBy: {property: 'sortKey', order: 'ASC'},
137131
});
138132

@@ -144,7 +138,7 @@ export default async function ActivitiesPage({
144138
<ActivitiesHero activityType={activityType as ActivityType} />
145139
<Suspense>
146140
<ActivityCatalog
147-
serializedOramaDb={await getSerializedOramaDatabase()}
141+
contentfulActivities={contentfulActivities}
148142
activities={await getAllActivities()}
149143
facets={await getSearchFacets()}
150144
/>

apps/marketing/src/components/contentful/activityCatalog/__tests__/activityCatalog.test.tsx

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,15 @@ jest.mock(
2929

3030
// Mock Orama and plugin-data-persistence
3131
jest.mock('@orama/orama', () => ({
32+
create: jest.fn(),
33+
insertMultiple: jest.fn(),
3234
search: jest.fn().mockResolvedValue({
3335
hits: [
3436
{document: {id: 1, title: 'Test Activity', languagesText: 'English'}},
3537
{document: {id: 2, title: 'Another Activity', languagesText: 'Spanish'}},
3638
],
3739
}),
3840
}));
39-
jest.mock('@orama/plugin-data-persistence', () => ({
40-
restore: jest.fn().mockResolvedValue({}),
41-
}));
4241

4342
// Mock next/navigation
4443
jest.mock('next/navigation', () => ({
@@ -56,6 +55,84 @@ const mockFacets: any = {
5655

5756
const useSearchParamsMock = useSearchParams as jest.Mock;
5857

58+
// Adjust mockContentfulActivities to match the Activity type
59+
const mockContentfulActivities: any = [
60+
{
61+
sys: {
62+
id: '1',
63+
type: 'Entry',
64+
createdAt: '2025-01-01T00:00:00Z',
65+
updatedAt: '2025-01-01T00:00:00Z',
66+
locale: 'en-US',
67+
contentType: {
68+
sys: {id: 'activity', type: 'Link', linkType: 'ContentType'},
69+
},
70+
revision: 1,
71+
space: {sys: {id: 'space-id', type: 'Link', linkType: 'Space'}},
72+
environment: {sys: {id: 'env-id', type: 'Link', linkType: 'Environment'}},
73+
publishedVersion: 1,
74+
},
75+
metadata: {tags: []},
76+
fields: {
77+
title: 'Test Activity',
78+
image: 'https://localhost/test-image.jpg',
79+
organization: ['Test Organization'],
80+
ages: ['10-12'],
81+
languageProgramming: ['JavaScript'],
82+
shortDescription: 'A short description.',
83+
longDescription: 'A long description.',
84+
primaryLinkRef: 'https://example.com',
85+
technologyClassroom: ['Tech'],
86+
topic: ['Topic'],
87+
activityType: ['hour-of-code'],
88+
length: ['1 hour'],
89+
accessibilitys: ['Accessible'],
90+
languagesText: 'English',
91+
supportedLanguages: ['English'],
92+
standards: 'Standard',
93+
tutorialID: 'tutorial-1',
94+
featuredPosition: 1,
95+
},
96+
},
97+
{
98+
sys: {
99+
id: '2',
100+
type: 'Entry',
101+
createdAt: '2025-01-02T00:00:00Z',
102+
updatedAt: '2025-01-02T00:00:00Z',
103+
locale: 'en-US',
104+
contentType: {
105+
sys: {id: 'activity', type: 'Link', linkType: 'ContentType'},
106+
},
107+
revision: 1,
108+
space: {sys: {id: 'space-id', type: 'Link', linkType: 'Space'}},
109+
environment: {sys: {id: 'env-id', type: 'Link', linkType: 'Environment'}},
110+
publishedVersion: 1,
111+
},
112+
metadata: {tags: []},
113+
fields: {
114+
title: 'Another Activity',
115+
image: 'https://localhost/another-image.jpg',
116+
organization: ['Another Organization'],
117+
ages: ['13-15'],
118+
languageProgramming: ['Python'],
119+
shortDescription: 'Another short description.',
120+
longDescription: 'Another long description.',
121+
primaryLinkRef: 'https://example.org',
122+
technologyClassroom: ['Another Tech'],
123+
topic: ['Another Topic'],
124+
activityType: ['hour-of-ai'],
125+
length: ['2 hours'],
126+
accessibilitys: ['Accessible'],
127+
languagesText: 'Spanish',
128+
supportedLanguages: ['Spanish'],
129+
standards: 'Another Standard',
130+
tutorialID: 'tutorial-2',
131+
featuredPosition: 2,
132+
},
133+
},
134+
];
135+
59136
describe('ActivityCatalog', () => {
60137
beforeEach(() => {
61138
useSearchParamsMock.mockReturnValue({
@@ -72,7 +149,7 @@ describe('ActivityCatalog', () => {
72149
it('renders FacetDrawer, FacetBar, and ActivityCollection', async () => {
73150
render(
74151
<ActivityCatalog
75-
serializedOramaDb="{}"
152+
contentfulActivities={mockContentfulActivities}
76153
activities={mockActivities}
77154
facets={mockFacets}
78155
/>,
@@ -85,7 +162,7 @@ describe('ActivityCatalog', () => {
85162
it('updates searchTerm when typing in search box', async () => {
86163
render(
87164
<ActivityCatalog
88-
serializedOramaDb="{}"
165+
contentfulActivities={mockContentfulActivities}
89166
activities={mockActivities}
90167
facets={mockFacets}
91168
/>,
@@ -101,7 +178,7 @@ describe('ActivityCatalog', () => {
101178
it('opens and closes facet drawer', () => {
102179
render(
103180
<ActivityCatalog
104-
serializedOramaDb="{}"
181+
contentfulActivities={mockContentfulActivities}
105182
activities={mockActivities}
106183
facets={mockFacets}
107184
/>,
@@ -124,7 +201,7 @@ describe('ActivityCatalog', () => {
124201
});
125202
render(
126203
<ActivityCatalog
127-
serializedOramaDb="{}"
204+
contentfulActivities={mockContentfulActivities}
128205
activities={mockActivities}
129206
facets={mockFacets}
130207
/>,
@@ -145,7 +222,7 @@ describe('ActivityCatalog', () => {
145222
});
146223
render(
147224
<ActivityCatalog
148-
serializedOramaDb="{}"
225+
contentfulActivities={mockContentfulActivities}
149226
activities={mockActivities}
150227
facets={mockFacets}
151228
/>,
@@ -161,7 +238,7 @@ describe('ActivityCatalog', () => {
161238
it('renders with undefined facets', () => {
162239
render(
163240
<ActivityCatalog
164-
serializedOramaDb="{}"
241+
contentfulActivities={mockContentfulActivities}
165242
activities={mockActivities}
166243
facets={undefined}
167244
/>,
@@ -173,7 +250,7 @@ describe('ActivityCatalog', () => {
173250
it('search box has correct aria-label', () => {
174251
render(
175252
<ActivityCatalog
176-
serializedOramaDb="{}"
253+
contentfulActivities={mockContentfulActivities}
177254
activities={mockActivities}
178255
facets={mockFacets}
179256
/>,

apps/marketing/src/components/contentful/activityCatalog/activityCatalog.tsx

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,63 +4,51 @@ import Box from '@mui/material/Box';
44
import Button from '@mui/material/Button';
55
import Grid from '@mui/material/Grid';
66
import TextField from '@mui/material/TextField';
7-
import {FacetResult, InternalTypedDocument, Orama, search} from '@orama/orama';
8-
import {restore} from '@orama/plugin-data-persistence';
7+
import {FacetResult, InternalTypedDocument, search} from '@orama/orama';
98
import {useSearchParams} from 'next/navigation';
109
import {ChangeEvent, ComponentProps, useEffect, useState} from 'react';
1110
import {useDebouncedCallback} from 'use-debounce';
1211

1312
import FacetBar from '@/components/contentful/activityCatalog/facetBar/facetBar';
1413
import FacetDrawer from '@/components/contentful/activityCatalog/facetDrawer/facetDrawer';
1514
import ActivityCollection from '@/components/csforall/activityCollection/ActivityCollection';
16-
import {ActivitySchema} from '@/modules/activityCatalog/orama/schema/ActivitySchema';
17-
import {OramaActivity} from '@/modules/activityCatalog/types/Activity';
15+
import {createDatabase} from '@/modules/activityCatalog/orama/createDatabase';
16+
import {
17+
Activity,
18+
OramaActivity,
19+
} from '@/modules/activityCatalog/types/Activity';
20+
import {Entry} from '@/types/contentful/Entry';
1821

1922
import {FACET_CONFIG} from './config/facets';
2023

2124
interface ActivityCatalogProps {
22-
serializedOramaDb: string | ArrayBuffer | Buffer<ArrayBuffer>;
25+
contentfulActivities: Entry<Activity>[];
2326
activities: InternalTypedDocument<OramaActivity>[];
2427
facets: FacetResult | undefined;
2528
}
2629

2730
const ActivityCatalog = ({
28-
serializedOramaDb,
31+
contentfulActivities,
2932
activities,
3033
facets,
3134
}: ActivityCatalogProps) => {
3235
const allowedFacetSet = new Set(facets ? Object.keys(facets) : []);
3336

3437
const [results, setResults] =
3538
useState<InternalTypedDocument<OramaActivity>[]>(activities);
36-
const [db, setDb] = useState<Orama<typeof ActivitySchema> | undefined>(
37-
undefined,
38-
);
3939
const [selectedFacets, setSelectedFacets] = useState<
4040
Record<string, Set<string>>
4141
>({});
4242
const [searchTerm, setSearchTerm] = useState<string>('');
4343
const [isFacetDrawerOpen, setIsFacetDrawerOpen] = useState<boolean>(false);
4444

4545
const searchParams = useSearchParams();
46-
47-
// On load, restore the Orama database from the serialized data from the server on the browser.
48-
useEffect(() => {
49-
restore<Orama<typeof ActivitySchema>>('json', serializedOramaDb).then(
50-
restoredDb => {
51-
setDb(restoredDb);
52-
deserializeClientState();
53-
},
54-
);
55-
}, []);
46+
const db = createDatabase(contentfulActivities);
5647

5748
// Populate selected facets and search term from the URL search params on load and when they change.
5849
useEffect(() => {
59-
// The database may not be loaded yet, only hydrate when it is.
60-
if (db) {
61-
deserializeClientState();
62-
}
63-
}, [db, searchParams]);
50+
deserializeClientState();
51+
}, [searchParams]);
6452

6553
/**
6654
* Hydrates the client state (search term and selected facets) from the URL search params.

yarn.lock

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,7 +1672,6 @@ __metadata:
16721672
"@opentelemetry/sdk-logs": "npm:^0.208.0"
16731673
"@opentelemetry/sdk-metrics": "npm:^2.2.0"
16741674
"@orama/orama": "npm:^3.1.14"
1675-
"@orama/plugin-data-persistence": "npm:^3.1.14"
16761675
"@playwright/test": "npm:~1.49.1"
16771676
"@statsig/js-client": "npm:^3.30.0"
16781677
"@statsig/react-bindings": "npm:^3.30.0"
@@ -4696,13 +4695,6 @@ __metadata:
46964695
languageName: node
46974696
linkType: hard
46984697

4699-
"@msgpack/msgpack@npm:^3.1.2":
4700-
version: 3.1.2
4701-
resolution: "@msgpack/msgpack@npm:3.1.2"
4702-
checksum: 10c0/4fee6dbea70a485d3a787ac76dd43687f489d662f22919237db1f2abbc3c88070c1d3ad78417ce6e764bcd041051680284654021f52068e0aff82d570cb942d5
4703-
languageName: node
4704-
linkType: hard
4705-
47064698
"@mui/core-downloads-tracker@npm:^7.1.2":
47074699
version: 7.1.2
47084700
resolution: "@mui/core-downloads-tracker@npm:7.1.2"
@@ -6529,25 +6521,13 @@ __metadata:
65296521
languageName: node
65306522
linkType: hard
65316523

6532-
"@orama/orama@npm:3.1.14, @orama/orama@npm:^3.1.14":
6524+
"@orama/orama@npm:^3.1.14":
65336525
version: 3.1.14
65346526
resolution: "@orama/orama@npm:3.1.14"
65356527
checksum: 10c0/ae3605613331117aa426a544f0b50d6e4293e703e851e4b2f7f8c9400e597475aec80c662ed3db8a68d22d8f36fe5a65aa9c45870a3508bfd13c4b44032eb53a
65366528
languageName: node
65376529
linkType: hard
65386530

6539-
"@orama/plugin-data-persistence@npm:^3.1.14":
6540-
version: 3.1.14
6541-
resolution: "@orama/plugin-data-persistence@npm:3.1.14"
6542-
dependencies:
6543-
"@msgpack/msgpack": "npm:^3.1.2"
6544-
"@orama/orama": "npm:3.1.14"
6545-
dpack: "npm:^0.6.22"
6546-
seqproto: "npm:^0.2.3"
6547-
checksum: 10c0/e72b46a4f7c505f42a11c166f5a0c89b9029413771e71616d003c8ba94163bb07b76795154076786165997da8f6c50ff00664b61786be347895eefc70b991c51
6548-
languageName: node
6549-
linkType: hard
6550-
65516531
"@parcel/watcher-android-arm64@npm:2.5.0":
65526532
version: 2.5.0
65536533
resolution: "@parcel/watcher-android-arm64@npm:2.5.0"
@@ -12653,13 +12633,6 @@ __metadata:
1265312633
languageName: node
1265412634
linkType: hard
1265512635

12656-
"dpack@npm:^0.6.22":
12657-
version: 0.6.22
12658-
resolution: "dpack@npm:0.6.22"
12659-
checksum: 10c0/ca4d74a6b437d9f4dd0cb5beee1bc501272a20ac524ec6d4b6c01d7b3d86fc164f6cc648c0c18057c337c1225aca05985b75bda94d4f28adabdeb2bb4c9bcfe3
12660-
languageName: node
12661-
linkType: hard
12662-
1266312636
"dunder-proto@npm:^1.0.1":
1266412637
version: 1.0.1
1266512638
resolution: "dunder-proto@npm:1.0.1"
@@ -21599,13 +21572,6 @@ __metadata:
2159921572
languageName: node
2160021573
linkType: hard
2160121574

21602-
"seqproto@npm:^0.2.3":
21603-
version: 0.2.3
21604-
resolution: "seqproto@npm:0.2.3"
21605-
checksum: 10c0/d0f8c133c495261d08563bbf253b8f29736b02b7add33f2c72bcdb97821cb41b4d6231ec2986fac596685e5b9312e4366242536ab5cb551c405a13402a1f1e1a
21606-
languageName: node
21607-
linkType: hard
21608-
2160921575
"serialize-javascript@npm:^6.0.1":
2161021576
version: 6.0.2
2161121577
resolution: "serialize-javascript@npm:6.0.2"

0 commit comments

Comments
 (0)