DEV Community

Wild Animal
Wild Animal

Posted on

Understanding CDN Cache in NextJs

Understanding CDN Cache in Next.js

Next.js has emerged as a popular framework for building fast, user-friendly web applications. The framework claims to automatically and proactively handle caching within applications. However, is this really the case?

Recently, I encountered a challenge while developing my book review website, refanswer.com. To reduce the load on the server when accessing the Firestore database, I needed to implement caching between requests.

Next.js indeed implements a caching mechanism for server-side request data. This can be achieved using the optimized fetch method to access API URLs for data retrieval or by using the cache method provided by React to wrap data-fetching methods from third-party libraries.

You can find detailed instructions for the former here, and for the latter here.

// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })
Enter fullscreen mode Exit fullscreen mode
import { cache } from 'react';
import db from '@/lib/db';

export const getItem = cache(async (id: string) => {
  const item = await db.item.findUnique({ id });
  return item;
});
Enter fullscreen mode Exit fullscreen mode

These methods address the issue of repeatedly requesting the same data within a single request. For example, the generateMetadata method and the server component might both use the same ID to fetch data from the database or API.

However, the lifecycle of these caching methods is limited to individual requests. In other words, the cached data is only available within the same request and cannot be shared across different requests. While this is useful and significantly reduces the number of requests between the server and the data source, I had a higher requirement: I wanted to share data across multiple user requests to the server.

This concept is similar to that of a CDN (Content Delivery Network). However, the Next.js documentation does not emphasize this aspect. Given Vercel's reputation for providing edge network services, Vercel seems to be the default solution to this problem. Yet, without further configuration, Vercel will not proactively resolve these issues.

To achieve this functionality, you need to configure the Cache-Control headers in next.config.mjs, vercel.json, or middleware.ts.

Key Cache-Control Directives

max-age:

Specifies the maximum time a resource is considered fresh in both browser and shared caches. It has a lower priority compared to s-maxage and is commonly used to control the overall caching duration for a resource across all caching layers.

s-maxage:

Specifies the maximum time a resource is considered fresh in shared caches only. It takes precedence over max-age for these caches and is typically used to extend or reduce the caching duration specifically for proxy servers to optimize content delivery.

In summary, max-age has a lower priority and is typically used to set cache durations for both browser and shared caches, while s-maxage has a higher priority and is specifically used to set cache durations for shared caches in the network.

Configuring Cache-Control

To configure cache-control, set the values of max-age and s-maxage according to your requirements. A typical configuration in middleware might look like this:

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Set Cache-Control headers
  response.headers.set('Cache-Control', 'public, max-age=3600, s-maxage=7200, stale-while-revalidate=59');

  return response;
}
Enter fullscreen mode Exit fullscreen mode

The stale-while-revalidate directive specifies a time window during which the cache can continue to use a resource even if it has expired, while revalidating and updating the resource in the background. I won’t go into too much detail about it here.

In addition to setting Cache-Control headers in middleware, you can also set them in next.config.mjs and vercel.json (only valid for the Vercel network). For example:

In next.config.mjs:

headers: async () => {
  return [
    {
      source: "/(.*)",
      headers: [
        {
          key: "Cache-Control",
          value: "public, max-age=60, s-maxage=3600, stale-while-revalidate=60",
        },
      ],
    },
  ];
},
Enter fullscreen mode Exit fullscreen mode

In vercel.json:

{
  "headers": [
    {
      "source": "/about.js",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "s-maxage=1, stale-while-revalidate=59"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

For more details, please visit: Vercel Caching Documentation.

About Dynamic Routing

In a dynamic route page, such as app/[slug]/page.tsx, setting Cache-Control headers alone is not sufficient. You must also simulate the dynamic route page as a static page. A simple way to achieve this is by using the following syntax:

export const dynamic = "force-static";
export const revalidate = 86400;
Enter fullscreen mode Exit fullscreen mode

This makes the page a static page, allowing the Cache-Control headers in the response to take effect.

Image description

Top comments (0)