Course
Router Cache
Next.js Mastery: From Fundamentals to Full-Stack
Unlock the power of Next.js with this comprehensive course! Starting with the basics, you’ll learn essential skills such as routing, data fetching, and styling. Progress through practical projects, including building your own React Notes app, to gain hands-on experience. Dive into the source code to understand the inner workings and core principles of Next.js. Perfect for developers with a basic knowledge of React and Node.js, this course ensures you’ll be ready to create high-performance full-stack applications efficiently. Join us and master Next.js, backed by industry experts and a supportive learning community.
Router Cache
1. How It Works
Next.js maintains a client-side cache stored in memory during a user session, where it caches the React Server Component Payload (RSC Payload) by route segment. This is known as the router cache.
Here’s a visual representation of how it works:
The diagram is straightforward: when you first access route
/a
(MISS), Next.js caches the layout (/
) and page (/a
) segments in the router cache (SET). When you navigate to route /b
, which shares the layout with /a
, Next.js reuses the layout from the router cache and caches the page segment (/b
). When you navigate back to /a
, it directly uses the cached layout and page segments (HIT).In addition, when users navigate between routes, Next.js caches visited route segments and prefetches routes that users might navigate to (based on
<Link>
components within the viewport). This enhances the navigation experience by:- Enabling instant forward and backward navigation since previously visited routes are cached, and new routes are preloaded.
- Avoiding page reloads during navigation while preserving React and browser state.
Let’s verify this with a demo:
// app/layout.jsimport Link from "next/link";
export default function RootLayout({ children }) { return ( <html lang="en"> <body> <div> <Link href="/a">Link to /a</Link> <br /> <Link href="/b">Link to /b</Link> </div> {children} </body> </html> )}
Here’s the code for both routes:
// app/a/page.js | app/b/page.jsexport default function Page() { return ( <h1>Component X</h1> )}
When you first visit
/a
, both /a
and /b
are preloaded because the <Link>
components for both are within the viewport.
Thanks to preloading and caching, navigating back and forth between these routes is smooth:
2. Duration
Router cache is stored in the browser’s temporary cache and lasts for the duration of the user session. There are two factors that determine its duration:
- Session: The cache persists during navigation but is cleared when the page is refreshed.
- Automatic Expiration: Each route segment automatically expires after a specific period:
- Static Rendering: 5 minutes.
- Dynamic Rendering: 30 seconds.
For example, in the demo above, if you wait 5 minutes and then click the links again, the RSC Payload will be re-fetched.
By adding
prefetch={true}
(default for <Link>
components) or calling router.prefetch
in dynamic routes, the cache can last for 5 minutes.3. Invalidation
Router cache can be invalidated in a few ways:
- In Server Actions:
- Using
revalidatePath
orrevalidateTag
to revalidate data. - Calling
cookies.set
orcookies.delete
, which invalidates the router cache to prevent outdated routes (such as those related to authentication). - Calling
router.refresh
, which invalidates the router cache and triggers a fresh request for the current route.
4. Opting Out
You cannot fully opt out of the router cache. However, you can pass
false
to the <Link>
component’s prefetch
prop to disable prefetching. The route segment will still be temporarily stored for 30 seconds to facilitate instant navigation between nested route segments. Also, visited routes are still cached.5. Real-World Experience
While router cache may seem beneficial, let’s look at a scenario where it can be problematic.
Consider the following directory structure:
app ├─ (cache) │ ├─ about │ │ └─ page.js │ ├─ settings │ │ └─ page.js │ ├─ layout.js │ └─ loading.js
Here’s the code for the layout:
// app/(cache)/layout.jsimport Link from 'next/link'
export const dynamic = 'force-dynamic'
export default function CacheLayout({ children,}) { return ( <section className="p-5"> <nav className="flex items-center justify-center gap-10 text-blue-600 mb-6"> <Link href="/about">About</Link> <Link href="/settings">Settings</Link> </nav> {children} </section> )}
And the code for the loading state:
// app/(cache)/loading.jsexport default function DashboardLoading() { return <div className="h-60 flex-1 rounded-xl bg-indigo-500 text-white flex items-center justify-center">Loading</div>}
Finally, the code for the
/about
and /settings
pages:// app/(cache)/about/page.jsconst sleep = ms => new Promise(r => setTimeout(r, ms));
export default async function About() { await sleep(2000) return ( <div className="h-60 flex-1 rounded-xl bg-teal-400 text-white flex items-center justify-center">Hello, About! {new Date().toLocaleString()}</div> )}
// app/(cache)/settings/page.jsconst sleep = ms => new Promise(r => setTimeout(r, ms));
export default async function Settings() { await sleep(2000) return ( <div className="h-60 flex-1 rounded-xl bg-teal-400 text-white flex items-center justify-center">Hello, Settings! {new Date().toLocaleString()}</div> )}
Running this in production, you’ll notice the following behavior:
At first glance, the interaction seems fine, but notice:
- When you refresh the page,
/about
shows a loading state. When you first navigate to/settings
, it also shows a loading state. However, subsequent clicks between/about
and/settings
do not trigger the loading state. - Also, there are no network requests after the first navigation.
This is due to the client-side router cache. Since the RSC payload is cached, navigating between these routes doesn’t trigger new requests, and the displayed time remains unchanged.
How to Ensure Fresh Data?
- Wait: Router cache has an automatic expiration—30 seconds for dynamic routes, 5 minutes for static routes. Waiting 30 seconds before clicking again will fetch fresh data.
2. Use
<a>
Tags Instead of <Link>
: Replace <Link>
components with <a>
tags to trigger a page reload, but this will result in a full page reload.// app/(cache)/layout.jsimport Link from 'next/link'
export const dynamic = 'force-dynamic'
export default function CacheLayout({ children,}) { return ( <section className="p-5"> <nav className="flex items-center justify-center gap-10 text-blue-600 mb-6"> <a href="/about">About</a> <a href="/settings">Settings</a> </nav> {children} </section> )}
This solution triggers a full page reload, ensuring fresh data.
3. Use
router.refresh
: Trigger a refresh of the current route using router.refresh
. This approach requires converting your layout to a client component.// app/(cache)/layout.js'use client'
import { useRouter } from 'next/navigation'
export default function CacheLayout({ children,}) { const router = useRouter() return ( <section className="p-5"> <nav className="flex items-center justify-center gap-10 text-blue-600 mb-6"> <button onClick={() => { router.push('/about') router.refresh() }}>About</button> <button onClick={() => { router.push('/settings') router.refresh() }}>Settings</button> </nav> {children} </section> )}
Adding
export const dynamic = 'force-dynamic'
to your pages app/(cache)/about/page.js and app/(cache)/about/page.js ensures they use dynamic rendering.export const dynamic = 'force-dynamic'
Running the production version, the effect is as follows:
4. Use
NavigationEvents
for Automatic Refresh:// app/(cache)/navigation-events.js'use client'
import { useEffect } from 'react'import { usePathname, useSearchParams } from 'next/navigation'import { useRouter } from 'next/navigation'
export function NavigationEvents() { const pathname = usePathname() const searchParams = useSearchParams() const router = useRouter() useEffect(() => { router.refresh() }, [pathname, searchParams])
return null}
Add this to your layout:
// app/(cache)/layout.jsimport Link from 'next/link'import { Suspense } from 'react'import { NavigationEvents } from './navigation-events'
export const dynamic = 'force-dynamic'
export default function CacheLayout({ children,}) { return ( <section className="p-5"> <nav className="flex items-center justify-center gap-10 text-blue-600 mb-6"> <Link href={`/about`}>About</Link> <Link href={`/settings`}>Settings</Link> </nav> {children} <Suspense fallback={null}> <NavigationEvents /> </Suspense> </section> )}
Running the production version, the effect is as follows:
Conclusion
Router cache differs from full route cache in that:
- Router cache happens during user sessions and temporarily stores RSC payloads in the browser. It lasts for the session and is cleared upon page refresh. Full route cache persists on the server and can be reused across multiple requests.
- Full route cache only applies to statically rendered routes, while router cache applies to both static and dynamic routes.
In practice, router cache can be both beneficial and challenging to manage. It’s frequently used, but since it cannot be fully disabled, you may need to implement special handling as shown above.
Next.js Caching Summary Table
In case of caching issues during development, refer to this table to identify the relevant caching type and choose an appropriate strategy for revalidation or opting out of caching.
References
- Building Your Application: Caching | Next.js