From 27401eda4ac155b3398f89c632c1c4cbf807b2ba Mon Sep 17 00:00:00 2001 From: Viet Nguyen <3805254+vnugent@users.noreply.github.com> Date: Tue, 30 Apr 2024 02:24:25 -0700 Subject: [PATCH] Improve landing page load time (#1124) * refactor: optimizing landing page. set revalidate at `fetch` call * refactor: use experimental apollo client for server component --- package.json | 3 +- src/app/(default)/area/[[...slug]]/page.tsx | 2 +- src/app/(default)/components/RecentTags.tsx | 4 +- src/app/(default)/components/Volunteers.tsx | 2 +- src/app/(default)/edit/page.tsx | 2 +- src/app/(default)/header.tsx | 5 +- src/app/(default)/page.tsx | 5 +- src/app/(maps)/components/ProfileMenu.tsx | 3 +- src/app/api/updateAreaPage/route.ts | 2 + src/js/graphql/Client.ts | 9 ++- src/js/graphql/ServerClient.ts | 23 ++++++ src/js/graphql/api.ts | 5 +- src/js/graphql/contribAPI.ts | 8 +-- src/js/graphql/getPopularAreasUSA.ts | 5 +- src/js/graphql/gql/serverApi.ts | 30 ++++++++ yarn.lock | 79 +++++++++++++++++---- 16 files changed, 152 insertions(+), 35 deletions(-) create mode 100644 src/js/graphql/ServerClient.ts create mode 100644 src/js/graphql/gql/serverApi.ts diff --git a/package.json b/package.json index a05649299..6fc3112e5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dependencies": { "@algolia/autocomplete-js": "1.7.1", "@algolia/autocomplete-theme-classic": "1.7.1", - "@apollo/client": "^3.7.16", + "@apollo/client": "^3.10.1", + "@apollo/experimental-nextjs-app-support": "^0.10.0", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", diff --git a/src/app/(default)/area/[[...slug]]/page.tsx b/src/app/(default)/area/[[...slug]]/page.tsx index 4ac5433ba..3a7c30586 100644 --- a/src/app/(default)/area/[[...slug]]/page.tsx +++ b/src/app/(default)/area/[[...slug]]/page.tsx @@ -22,7 +22,7 @@ import { AuthorMetadata, OrganizationType } from '@/js/types' /** * Page cache settings */ -export const revalidate = 86400 // 24 hours +export const revalidate = 300 // 5 mins export const fetchCache = 'force-no-store' // opt out of Nextjs version of 'fetch' interface PageSlugType { diff --git a/src/app/(default)/components/RecentTags.tsx b/src/app/(default)/components/RecentTags.tsx index 00ef6211d..f83743e24 100644 --- a/src/app/(default)/components/RecentTags.tsx +++ b/src/app/(default)/components/RecentTags.tsx @@ -1,11 +1,11 @@ import { RecentImageCard } from '@/components/home/RecentMediaCard' -import { getMediaForFeed } from '@/js/graphql/api' +import { getMediaForFeedSC } from '@/js/graphql/gql/serverApi' /** * Horizontal gallery of recent images with tags */ export const RecentTags: React.FC = async () => { - const recentTagsByUsers = await getMediaForFeed(20, 4) + const recentTagsByUsers = await getMediaForFeedSC(20, 4) const testAreaIds = Array.from(new Set((process.env.NEXT_PUBLIC_TEST_AREA_IDS ?? '').split(','))) const mediaWithTags = recentTagsByUsers.flatMap(entry => entry.mediaWithTags) diff --git a/src/app/(default)/components/Volunteers.tsx b/src/app/(default)/components/Volunteers.tsx index bf555e5d2..06b7548d9 100644 --- a/src/app/(default)/components/Volunteers.tsx +++ b/src/app/(default)/components/Volunteers.tsx @@ -18,7 +18,7 @@ interface GithubProfile { * managed by all-contributors bot. */ export const Volunteers: React.FC = async () => { - const res = await fetch(url, { cache: 'no-store' }) + const res = await fetch(url, { next: { revalidate: 84600 } }) const { contributors }: { contributors: GithubProfile[] } = await res.json() return ( { const history = await getChangeHistoryServerSide() diff --git a/src/app/(default)/header.tsx b/src/app/(default)/header.tsx index 62aadbb30..69fba14d9 100644 --- a/src/app/(default)/header.tsx +++ b/src/app/(default)/header.tsx @@ -1,4 +1,5 @@ import clx from 'classnames' +import Link from 'next/link' import OpenBetaLogo from '@/assets/brand/openbeta-logo' import { DesktopHeader } from './components/DesktopHeader' @@ -26,9 +27,9 @@ export enum LogoSize { */ export const Logo: React.FC<{ size?: LogoSize, className?: string, withText?: boolean }> = ({ size = LogoSize.sm, className, withText = false }) => { return ( - + {withText && OpenBeta} - + ) } diff --git a/src/app/(default)/page.tsx b/src/app/(default)/page.tsx index df06f4a76..3b1e20be5 100644 --- a/src/app/(default)/page.tsx +++ b/src/app/(default)/page.tsx @@ -9,10 +9,7 @@ import { InternationalToC } from './components/InternationalToC' import { Volunteers } from './components/Volunteers' import { RecentContributionsMap } from './components/recent/RecentContributionsMap' -/** - * Cache duration in seconds - */ -export const dynamic = 'force-dynamic' +export const revalidate = 3600 // 1 hour /** * Root home page diff --git a/src/app/(maps)/components/ProfileMenu.tsx b/src/app/(maps)/components/ProfileMenu.tsx index abb584fa8..f0c9e2527 100644 --- a/src/app/(maps)/components/ProfileMenu.tsx +++ b/src/app/(maps)/components/ProfileMenu.tsx @@ -1,4 +1,5 @@ 'use client' +import Link from 'next/link' import { SessionProvider } from 'next-auth/react' import { House } from '@phosphor-icons/react/dist/ssr' import AuthenticatedProfileNavButton from '@/components/AuthenticatedProfileNavButton' @@ -9,7 +10,7 @@ export const ProfileMenu: React.FC = () => {
diff --git a/src/app/api/updateAreaPage/route.ts b/src/app/api/updateAreaPage/route.ts index 96744b427..c64444678 100644 --- a/src/app/api/updateAreaPage/route.ts +++ b/src/app/api/updateAreaPage/route.ts @@ -15,6 +15,8 @@ export async function GET (request: NextRequest): Promise { } else { revalidatePath(`/area/${uuid}`, 'page') revalidatePath(`/editArea/${uuid}`, 'layout') + revalidatePath('/', 'page') + revalidatePath('/edit', 'page') return NextResponse.json({ message: 'OK' }, { status: 200 }) } } diff --git a/src/js/graphql/Client.ts b/src/js/graphql/Client.ts index 54106fba8..ec4501bae 100644 --- a/src/js/graphql/Client.ts +++ b/src/js/graphql/Client.ts @@ -113,6 +113,11 @@ if (openCollectiveApiKey !== '') { } export const openCollectiveClient = new ApolloClient({ - uri: openCollectiveUri, - cache: new InMemoryCache() + cache: new InMemoryCache(), + link: new HttpLink({ + uri: openCollectiveUri, + fetchOptions: { + next: { revalidate: 3600 } + } + }) }) diff --git a/src/js/graphql/ServerClient.ts b/src/js/graphql/ServerClient.ts new file mode 100644 index 000000000..b4825915e --- /dev/null +++ b/src/js/graphql/ServerClient.ts @@ -0,0 +1,23 @@ +import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client' +import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc' + +const uri: string = process.env.NEXT_PUBLIC_API_SERVER ?? '' + +if (uri === '' || uri == null) { + throw new Error('NEXT_PUBLIC_API_SERVER is not set') +} + +/** + * Apollo client to be used in server components. + */ +export const { getClient } = registerApolloClient(() => { + return new ApolloClient({ + cache: new InMemoryCache(), + link: new HttpLink({ + uri, + fetchOptions: { + next: { revalidate: 3600 } // a non-zero value enables 'public, max-age=0, must-revalidate' + } + }) + }) +}) diff --git a/src/js/graphql/api.ts b/src/js/graphql/api.ts index 2384de367..9c2ea4b3e 100644 --- a/src/js/graphql/api.ts +++ b/src/js/graphql/api.ts @@ -3,6 +3,7 @@ import AwesomeDebouncePromise from 'awesome-debounce-promise' import { AreaType, ClimbType, TickType, MediaByUsers, CountrySummaryType, MediaWithTags } from '../types' import { graphqlClient } from './Client' + import { CORE_CRAG_FIELDS, QUERY_CRAGS_WITHIN, QUERY_TICKS_BY_USER_AND_CLIMB, QUERY_TICKS_BY_USER, QUERY_ALL_COUNTRIES } from './gql/fragments' import { QUERY_MEDIA_FOR_FEED } from './gql/tags' import { QUERY_USER_MEDIA } from './gql/users' @@ -91,8 +92,8 @@ export const getMediaForFeed = async (maxUsers: number, maxFiles: number): Promi variables: { maxUsers, maxFiles - }, - fetchPolicy: 'network-only' + } + // fetchPolicy: 'network-only' }) if (Array.isArray(rs.data?.getMediaForFeed)) { diff --git a/src/js/graphql/contribAPI.ts b/src/js/graphql/contribAPI.ts index c55c91225..2dcb5f4ec 100644 --- a/src/js/graphql/contribAPI.ts +++ b/src/js/graphql/contribAPI.ts @@ -1,16 +1,16 @@ import { QUERY_RECENT_CHANGE_HISTORY } from './gql/contribs' -import { graphqlClient } from './Client' +import { getClient } from './ServerClient' import { ChangesetType } from '../types' export const getChangeHistoryServerSide = async (): Promise => { try { - const rs = await graphqlClient.query<{ getChangeHistory: ChangesetType[] }>({ + const rs = await getClient().query<{ getChangeHistory: ChangesetType[] }>({ query: QUERY_RECENT_CHANGE_HISTORY, - fetchPolicy: 'cache-first' + fetchPolicy: 'no-cache' }) if (Array.isArray(rs.data?.getChangeHistory)) { - return rs.data?.getChangeHistory.slice(0, 50) + return rs.data?.getChangeHistory.slice(0, 20) } console.log('WARNING: getChangeHistory() returns non-array data') return [] diff --git a/src/js/graphql/getPopularAreasUSA.ts b/src/js/graphql/getPopularAreasUSA.ts index a18417733..bca97826d 100644 --- a/src/js/graphql/getPopularAreasUSA.ts +++ b/src/js/graphql/getPopularAreasUSA.ts @@ -1,7 +1,8 @@ import { gql } from '@apollo/client' import { FRAGMENT_MEDIA_WITH_TAGS } from './gql/tags' -import { graphqlClient } from './Client' +import { getClient } from './ServerClient' + import { AreaType } from '../types' /** @@ -79,7 +80,7 @@ export interface USAToCProps { } export const getPopularAreasInUSA = async (): Promise => { - const rs = await graphqlClient.query({ + const rs = await getClient().query({ query, variables: { filter: { diff --git a/src/js/graphql/gql/serverApi.ts b/src/js/graphql/gql/serverApi.ts new file mode 100644 index 000000000..a711d2a20 --- /dev/null +++ b/src/js/graphql/gql/serverApi.ts @@ -0,0 +1,30 @@ +import { getClient } from '../ServerClient' +import { MediaByUsers } from '@/js/types' +import { QUERY_MEDIA_FOR_FEED } from './tags' + +/** + * Server component API: Get recent media for feed. + * @param maxUsers + * @param maxFiles + */ +export const getMediaForFeedSC = async (maxUsers: number, maxFiles: number): Promise => { + try { + const rs = await getClient().query<{ getMediaForFeed: MediaByUsers[] }>({ + query: QUERY_MEDIA_FOR_FEED, + variables: { + maxUsers, + maxFiles + }, + fetchPolicy: 'cache-first' + }) + + if (Array.isArray(rs.data?.getMediaForFeed)) { + return rs.data?.getMediaForFeed + } + console.log('WARNING: getMediaForFeed() returns non-array data') + return [] + } catch (e) { + console.log('####### getMediaForFeed() error', e) + } + return [] +} diff --git a/yarn.lock b/yarn.lock index 0b2e2e249..a9119d3e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -60,25 +60,41 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@apollo/client@^3.7.16": - version "3.8.4" - resolved "https://registry.npmjs.org/@apollo/client/-/client-3.8.4.tgz" - integrity sha512-QFXE4ylSHUa6LgYoOGsPysJCm4YJOOM1NwHyF6msZdZXIerqUVpLvxQOdQEXgS0RWvYiBMC1wGOWKzJKSWBdAg== +"@apollo/client-react-streaming@0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@apollo/client-react-streaming/-/client-react-streaming-0.10.0.tgz#1f9a818c679b41791ca02105f16525c93412994c" + integrity sha512-iZ2jYghRS71xFv6O3Js5Ojrrmk4SnIEKwPRKIswQyAtqjHrfvUTyXCDzxrhPcGQe/y7su/XcE7Xp0kOp7yTnlg== + dependencies: + superjson "^1.12.2 || ^2.0.0" + ts-invariant "^0.10.3" + +"@apollo/client@^3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.10.1.tgz#4c8eec28fcce25b96f27c1f1e443ec5c676e4de0" + integrity sha512-QNacQBZzJla5UQ/LLBXJWM7/1v1C5cfpMQPAFjW4hg4T54wHWbg4Dr+Dp6N+hy/ygu8tepdM+/y/5VFLZhovlQ== dependencies: "@graphql-typed-document-node/core" "^3.1.1" - "@wry/context" "^0.7.3" + "@wry/caches" "^1.0.0" "@wry/equality" "^0.5.6" - "@wry/trie" "^0.4.3" + "@wry/trie" "^0.5.0" graphql-tag "^2.12.6" hoist-non-react-statics "^3.3.2" - optimism "^0.17.5" + optimism "^0.18.0" prop-types "^15.7.2" + rehackt "^0.1.0" response-iterator "^0.2.6" symbol-observable "^4.0.0" ts-invariant "^0.10.3" tslib "^2.3.0" zen-observable-ts "^1.2.5" +"@apollo/experimental-nextjs-app-support@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@apollo/experimental-nextjs-app-support/-/experimental-nextjs-app-support-0.10.0.tgz#2fe52e6bc18de87c17bfc2bc78ec63acf8260c8f" + integrity sha512-S3mfZRnAAAaKwA8RNckS4TWYLX5utpmRTwG3WGFtpooYx8QQG8xft0p0a9eTQ53Jrw3nSMJc/wOOsT/5noMCQg== + dependencies: + "@apollo/client-react-streaming" "0.10.0" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.13": version "7.22.13" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz" @@ -3017,7 +3033,14 @@ resolved "https://registry.yarnpkg.com/@vercel/edge/-/edge-1.1.1.tgz#9b2fc0081dfe95db8b4c3598275721b1ad85e43f" integrity sha512-NtKiIbn9Cq6HWGy+qRudz28mz5nxfOJWls5Pnckjw1yCfSX8rhXdvY/il3Sy3Zd5n/sKCM2h7VSCCpJF/oaDrQ== -"@wry/context@^0.7.0", "@wry/context@^0.7.3": +"@wry/caches@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@wry/caches/-/caches-1.0.1.tgz#8641fd3b6e09230b86ce8b93558d44cf1ece7e52" + integrity sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA== + dependencies: + tslib "^2.3.0" + +"@wry/context@^0.7.0": version "0.7.3" resolved "https://registry.npmjs.org/@wry/context/-/context-0.7.3.tgz" integrity sha512-Nl8WTesHp89RF803Se9X3IiHjdmLBrIvPMaJkl+rKVJAYyPsz1TEUbu89943HpvujtSJgDUx9W4vZw3K1Mr3sA== @@ -3038,6 +3061,13 @@ dependencies: tslib "^2.3.0" +"@wry/trie@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.5.0.tgz#11e783f3a53f6e4cd1d42d2d1323f5bc3fa99c94" + integrity sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA== + dependencies: + tslib "^2.3.0" + "@xobotyi/scrollbar-width@^1.9.5": version "1.9.5" resolved "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz" @@ -3849,6 +3879,13 @@ cookiejar@^2.1.3: resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== +copy-anything@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0" + integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w== + dependencies: + is-what "^4.1.8" + copy-to-clipboard@^3.3.1: version "3.3.3" resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz" @@ -5847,6 +5884,11 @@ is-weakset@^2.0.1: call-bind "^1.0.2" get-intrinsic "^1.1.1" +is-what@^4.1.8: + version "4.1.16" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f" + integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== + isarray@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" @@ -7409,11 +7451,12 @@ openid-client@^5.4.0: object-hash "^2.2.0" oidc-token-hash "^5.0.3" -optimism@^0.17.5: - version "0.17.5" - resolved "https://registry.npmjs.org/optimism/-/optimism-0.17.5.tgz" - integrity sha512-TEcp8ZwK1RczmvMnvktxHSF2tKgMWjJ71xEFGX5ApLh67VsMSTy1ZUlipJw8W+KaqgOmQ+4pqwkeivY89j+4Vw== +optimism@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.18.0.tgz#e7bb38b24715f3fdad8a9a7fc18e999144bbfa63" + integrity sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ== dependencies: + "@wry/caches" "^1.0.0" "@wry/context" "^0.7.0" "@wry/trie" "^0.4.3" tslib "^2.3.0" @@ -8255,6 +8298,11 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" +rehackt@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.1.0.tgz#a7c5e289c87345f70da8728a7eb878e5d03c696b" + integrity sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw== + remark-parse@^11.0.0: version "11.0.0" resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz" @@ -8876,6 +8924,13 @@ supercluster@^8.0.0, supercluster@^8.0.1: dependencies: kdbush "^4.0.2" +"superjson@^1.12.2 || ^2.0.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.1.tgz#9377a7fa80fedb10c851c9dbffd942d4bcf79733" + integrity sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA== + dependencies: + copy-anything "^3.0.2" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"