Static Site Generation (SSG) with NextJS
November 07, 2020
While working on a side project, I decided to use SSR (via getServerSideProps
) to build out paginated paths. The reason for this decision was that data on these pages is fairly dynamic, so I thought SSG was out of the question. Plus I wasn’t sure how I would generate all of the pages at build time. The issue I ran into however was fairly slow metrics. The start render and First Contentful Paint were very poor at around 4.5 seconds:
Webpage Test SSR Vitals
After reading up on the NextJS documentation, looking and some of the NextJS examples (found here) and asking in the Github discussions section, I found out that I could use NextJS to statically generate the paths (i.e. /page/2
) as well as statically generate pages, while not needing to worry about stale data on the page.
Generating static pages with getStaticProps
The first problem to solve was to work out how to generate a page statically to improve the metrics above. Thankfully, this is fairly straight forward using getStaticProps
. The getStaticProps
method replaces getServerSideProps
and is exported at the bottom of pages in the same way. This allows you to carry out usual data fetches to populate a page at build time. For my personal project, I make calls to my GraphQL resolvers here to fetch information required in the page:
export async function getStaticProps({ params }) {
const apolloClient = await initApolloClient({})
const { data: allServers } = await apolloClient.query({
query: getServersQuery,
variables: { input: { page: parseInt(params.index), getSponsoredOnly: false } }
})
const apolloStaticCache = apolloClient.cache.extract()
return {
props: {
servers: allServers?.getServers,
page: params?.index,
apolloStaticCache
},
// update every 30 mins
revalidate: 1800 // Remove if using SSR
}
}
This allows me to call my getServersQuery
resolver, fetch the data I need at build time and have this route statically rendered. This data is then returned from this method, which is simply passed into my page component as props:
const PaginatedPage = ({ servers, page }) => {
...
// more code here and stuff
...
There are two other things to explain in the code snippet above, params
and revalidate
.
Using revalidate
to update statically generated page data
The optional revalidate
parameter allows for Incremental Static Regeneration, which essentially solves the issue of data going stale. With this paramaeter added to getStaticProps
, the page will be re-generated after a set number of seconds as traffic comes in. In my case above, every 1800 seconds (or 30 minutes) this page will be re-built once traffic hits the route, meaning I dont need to rebuild my entire application to get new data from the getServersQuery
resolver.
Building routes statically using getStaticPaths
This is where params
comes in. The second issue I faced was needing to generate multiple paginatable pages at build time. How would I know how many pages I needed to build? For example, if a company had 400 products, with 20 products per page, how would they know to build 20 pages, allowing a user to go to /page/15
and see a statically generated (fast) page of products. This is solved by getStaticPaths
.
getStaticPaths
allows you to generate the paths you need statically. In my case, I needed to work out how many pages I have, based on a calculation of the total amount of servers
divided by the amount of servers
per page. I would then generate an array of this total, and iterate over it using a map
to generate page index’s:
export async function getStaticPaths() {
const SERVERS_PER_PAGE = 20
...
...
// removed code here that gets totalServerCount
...
...
const numberOfPages = Math.ceil(totalServerCount / SERVERS_PER_PAGE)
// - 1 here because the array counts from 0
const paths = Array(numberOfPages - 1)
.fill('')
.map((_, index) => ({
// We + 2 here because pages start at page/2
// Need to use toString() because getStaticProps needs string
params: { index: (index + 2).toString() }
}))
// fallback: false means pages that don’t have the
// correct id will 404.
return { paths, fallback: true }
}
The return from getStaticPaths
is the paths that can be generated. This is passed into getStaticProps
as the param
prop, which then generates this path. AMAZING! But what is fallback
?
Building future routes using fallback
There is an issue with the explanation above. What happens if a company was to add 400 new products, and needed 20 new page routes for these (i.e. allowing a user to go to /page/55
). This is solved with fallback
.
If the above scenario was to happen, and a user was to visit /page/21
which wasnt generated in our first example, this page would cause the fallback
to occour. This will cause this route to be generated on the fly, and from then on be a static route. The initial user would see a loading screen, and this would need to be accounted for in the front end, but subsiquent users would see a statically generated page. I solve this using the following code in my page:
const PaginatedPage = ({ servers, page }) => {
const router = useRouter()
if (router.isFallback) {
return <span>Loading...</span>
}
next/router
allows us to access an isFallback
parameter, which is passed from getStaticPaths
fallback
. As you can see, this first render will see a span
with Loading… in it, but subsiquent users will see the statically generated path, if it exists, and a 404
if it does not.
NICE!
The result from the above was that page load speeds halved: