Caching in Next.js is hard to understand, but it doesn't have to be.
Next.js does a lot of caching under the hood to improve app performance. It does this by using different kind of caching mechanisms that all have their differences and use cases. Once you know the mechanisms you can better and easier understand how the caching as a whole works.
In this post I'll go through all four mechanisms and explain their differences. This will give you a deeper understanding of Next.js and help you make better decisions when designing your application.
Let's dive in!
1. Request Memoization
Common problem we have in React is that we need same data in multiple components of our app.
To solve this problem we can pass the data as props, add some kind of global state to store our data in or we can just fetch the data from an api in every component we need it. In my experience this can get complicated pretty fast and it is not efficient. So we need a better solution for this kind of situations.
Enter Fetch Memoization.
In Next.js we can indeed make a fetch request in each component that needs the data, without worrying about efficiency. This is because the fetch requests are memoized (=cached) inside server components. So whenever you make a fetch request inside a server component, the return value of it is cached.
For example
1st fetch to /products -> hits the api endpoint and returns the data which is then cached
2nd fetch to /products -> does not hit the api endpoint and data is returned from the cache
3rd fetch to /products -> does not hit the api endpoint and data is returned from the cache
The fetch request memoization lasts the lifetime of a request, so the cache won’t be persisted over requests.
Even though it might rise red flags at first, seeing same fetch request in multiple components, it is actually very good and efficient way to utilise data in your Next.js app.
2. Data Cache
Where fetch memoization cached the fetch requests per request, data cache persists the results of data fetches across incoming server requests and deployments.
When we make a fetch request in a server component, Next.js first checks if the fetch request is memoized. If not, it checks the data cache if the request is cached there. If not, only then will it make the request to the actual api.
GET /products -> is fetch memoized: false -> is in data cache: false -> hit the api
Data cache persists the cache across server requests and even deployments. This means that we have to define or explicitly tell it when to revalidate or clear the cache. Data cache can be revalidated using either time-based revalidation or on-demand revalidation.
Data cache shines when you have a page which data rarely changes e.g. blog post. Whereas for a comment block, using data cache is not ideal because the data changes more often.
We can opt out using data cache by giving cache: 'no-store' setting for the fetch request. This means the data will be fetched whenever the fetch is called.
3. Full Route Cache
This mechanism revolves around rendering and caching static pages during the build time rather than during runtime.
When you build your application, Next.js identifies pages that are static, pages that don’t use any dynamic data such as user-specific information or query parameters. These static pages will then be cached to full route cache. When a user then requests this kind of page, it will be served directly from the cache instead of it being rendered when request arrives, making the page much faster.
There are three ways to opt out of the full route cache
Using dynamic functions such as cookies, headers or searchParams
Opt out using data cache. If a page has a fetch request that is not cached, it will also opt out the full route cache.
Using “dynamic = ‘force-dynamic’” or “revalidate = 0” in route segment config options.
Places where full route cache works well is e.g. blog post page. Yes it uses probably data from an api or database, but it rarely changes and it is data that we already have in build time.
Full route cache does persists across requests but not across deployments. This is why if we want to clear the cache, we need to either invalidate the cache or re-build & deploy our application. Invalidating can be done with on-demand revalidation to the data cache.
It's important to note also that the Next.js full route cache is effective only in production environments. During development, requests are dynamically rendered, meaning they are not saved to full route cache, to facilitate rapid iteration and debugging.
4. Router Cache
Router cache is used for caching route segments in the browser, so unlike the previous mechanisms this one saves the cached data to the browser.
Next.js automatically caches visited routes, but also possible future routes by prefetching all the pages that are pointed to by Link components on the given page. If the Link component is in the viewport, page it is pointing to will be pre-fetched and cached to the router cache.
Thanks to router cache, there is no full-page reloads between navigations and we get the instant backward / forward navigation that we are used to with Next.js.
There is no way to opt out using router cache, but we can invalidate it by calling router.refresh, revalidatePath or revalidateTag. Cache will be cleared on a page refresh too.
Conclusion
Next.js uses these four mechanisms under the hood to handle caching of our applications.
As we see it is not super clear and simple on the first glance how they work. Understanding at least the basics of them and how they work together, will in my opinion help you make more informed decisions when faced with design choices with your own applications.
Hopefully this has helped you at least a bit to understand how caching works in Next.js.
Here is an excellent table from Next.js caching docs, summarising the four mechanisms.
If you like posts like this, you might enjoy my exclusive newsletter.
In-depth web dev content I don't talk about anywhere else
Exclusive Q&A videos only for newsletter subscribers
Lessons learned working +10 years as a web developer
Join here: https://bit.ly/tk-signup