Reusing responses at build time
In some applications, like an e-commerce we have to query our database for list
and fetch
operations that returns the same data schema. But given that we usually have to call this methods inside getStaticProps
and getStaticPaths
we end up querying our services way more times than needed.
Imagine the following scenario. We have an e-commerce with 100 products and two routes that we statically generate at build time, an /
route that list all the products and a /[id]
route that shows the product details. We asume that we have an endpoint that returns all the products with the relevant data we need to pre-render our page. Every roundtrip to our service takes 150ms and consumes 1kb of bandwidth per product.
The graph below ilustrates how getStaticProps
makes a call to the DB for the listing page and for each product:
- getStaticPaths: 100kb bandwidth, 150ms execution time, 1 call.
- getStaticProps: 100kb bandwidth, 15000ms execution time, 100 calls.
- Total: 200kb bandwidth, 15150ms execution time, 101 calls.
But what if we could reuse the response from getStaticPaths
in our getStaticProps
calls?
- getStaticPaths: 100kb bandwidth, 150ms execution time, 1 call.
- getStaticProps: 0kb bandwidth, ~ 0ms execution time. 0 calls.
- Total: 100kb bandwidth, 150ms execution time, 1 call.
And what if we can reuse that cache at application level?
A real-world example
Lets start with some unoptimized code for our /[id]
page.
export const getStaticPaths = async () => { const products = await api.list() return { paths: products.map(product => ({ params: { id: product.id }, })), fallback: 'blocking' } } export const getStaticProps = async ({params}) => { const product = await api.fetch(params.id) if (!product) { return { notFound: true } } return { props: { product } } }
Lets add a cache using fs
to share state at build time between getStaticPaths
and getStaticProps
. We will add a cache
property to api
with a get
and a set
method to interact with the cache.
const api = { list: async () => { return PRODUCTS }, fetch: async (id: Product['id']) => { return PRODUCTS.find((product) => product.id === id) }, cache: { get: async (id: string): Promise<Product | null | undefined> => { const data = await fs.readFile(path.join(process.cwd(), 'products.db')) const products: Product[] = JSON.parse(data as unknown as string) return products.find((product) => product.id === id) }, set: async (products: Product[]) => { return await fs.writeFile( path.join(process.cwd(), 'products.db'), JSON.stringify(products) ) }, }, }
And we will use this methods in getStaticPaths
and getStaticProps
:
export const getStaticPaths = async () => { const products = await api.list() await api.cache.set(products) return { paths: products.map(product => ({ params: { id: product.id }, })), fallback: 'blocking' } } export const getStaticProps = async ({params}) => { let product = await cache.get(params.id); if (!product) { product = await api.fetch(params.id) } if (!product) { return { notFound: true } } return { props: { product } } }
That way we ensure to use always information cache-first and if we don't find it, we fallback to calling the API. If you want to optimize this to be cross-page you can move the cache to other file and reuse it.
But there is something else we should take care of. Our current code might collide with our revalidation process in case we do ISR, so we want to ensure to only read from cache if we are at build time.
import { PHASE_PRODUCTION_BUILD } from 'next/constants'; ... export const getStaticPaths = async () => { const products = await api.list() if (process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD) { await api.cache.set(products) } return { paths: products.map(product => ({ params: { id: product.id }, })), fallback: 'blocking' } } export const getStaticProps = async ({params}) => { let product = await cache.get(params.id); if (!product) { product = await api.fetch(params.id) } if (!product) { return { notFound: true } } return { props: { product } } }
Now we check if NEXT_PHASE
is PHASE_PRODUCTION_BUILD
so we know we only write to cache at build time. If you want to always cache build-time responses instead of manually caching at page level, you can move the usage of the cache methods to the level needed for your application.
This is a list to our products, each one will redirect to the /[id]
route that was generated reusing responses from the cache at build time.