Workshop

Caching in Next.js 15

Caching and revalidating in Next.js 15

What is Caching?

You surely have heard about caching. Browsers have cache, some apps have cache, you are probably using some function to “clear cache” to speed up you phone, but what is caching, exactly?

Caching is a process of storing data in a temporary storage for future quicker access.

What does it mean for you, a Next.js web developer?

Let’s say you’ve made a cooking recipes app. When user enters a page with recipes list, you fetch() it for them from some API.

And this particular API? It’s very slow. 🐌

Like, very, very slow – it takes 10 seconds to respond, and during this time your user sees nothing but “Loading…” Sounds like awful user experience, right?

The recipes aren’t changing that often, and the new ones are being added every few days – that’s where caching comes into play. You don’t have to fetch() the data every time user enters the list. You can cache it, and serve the list straight from your app, omitting the terribly slow API. User gets recipes instantly, and you don’t even have to make the API call, saving your monthly 1000 calls from the free plan limit. 😅

It's just one of the caching mechanisms available in Next.js to make your app lightning fast. We’ll go over them below.

Data Caching

I’ll assume that you are already familiar with data fetching methods, and differences between client and server components in Next.js. In previous versions of the framework all the ways to fetch data were cached by default. In Next.js 15 the approach was changed – now nothing is cached by default, and we have to opt in to take advantage of the cache.

Let’s consider the above example: We want to show a list of available recipes to the user. It’s a simple app, there is no interactivity, so we use a server component

page.tsx
interface Recipe {
  id: string;
  title: string;
}

const Recipes = async () => {
  const data = await fetch("https://api.example.com/recipes");
  const recipes: Recipe[] = await data.json();
  return (
    <div>
      <h1>Recipes:</h1>
      <ul>
        {recipes.map((recipe) => (
          <li key={recipe.id}>{recipe.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Recipes;

You run the dev server, everything works fine so you build and deploy your app. The list is of recipes is still there, but when the new recipes are added to the API, they are not showing up on your page. What’s going on, the data shouldn’t be cached by default in Next.js 15, where are the new recipes?

Well, the data is not being cached, but the page isn’t using any Dynamic API, so Next treats it as a static page and renders it once during build, running the fetch() then, to populate the recipes list. In that case we still need to explicitly use the default cache: “no-store” fetch option in order to make the page dynamically rendered.

page.tsx
export const dynamic = "force-dynamic"; // this will force the page to be dynamically rendered

interface Recipe {
  id: string;
  title: string;
}

const Recipes = async () => {
  const data = await fetch("https://api.example.com/recipes", {
    cache: "no-store", // this option will make the page dynamic as well
  });
  const recipes: Recipe[] = await data.json();
  return (
    <div>
      <h1>Recipes:</h1>
      <ul>
        {recipes.map((recipe) => (
          <li key={recipe.id}>{recipe.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Recipes;

But now we're back to the beginning. Data is being fetched from the API every time user enters the page, and our app needs to wait for the response. That’s where Incremental Static Regeneration (ISR) comes into play.

Revalidation

Next.js has a way to update the static content without rebuilding the entire site. It reduces the server load by serving prerendered, static pages for most requests, and rebuilding only the parts that require to be updated dynamically. Taking advantage of this feature requires literally just one line of code:

page.tsx
export const revalidate = 3600; // Time in seconds between content revalidations

interface Recipe {
  id: string;
  title: string;
}

const Recipes = async () => {
  const data = await fetch("https://api.example.com/recipes", {
    next: { revalidate: 30 }, // You can also use the revalide as a fetch option
  });
  const recipes: Recipe[] = await data.json();
  return (
    <div>
      <h1>Recipes:</h1>
      <ul>
        {recipes.map((recipe) => (
          <li key={recipe.id}>{recipe.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Recipes;

What is exactly happening there?

  1. During next build the page is generated with initial fetch() to get the recipes list.
  2. When user enters the page, Next serves them a static page with cached data. There is no call to API.
  3. After 1 hour (3600 seconds) has passed, the next user that enters the page will trigger the ISR – cache is going to be revalidated and the page is going to be regenerated in the background.
  4. After the regeneration, Next.js will display and cache the updated page.

The users have access to the fresh data almost instantly, your app is not bombarding the API with constant requests, and the content is served statically which allows search engines like Google to index your page not only using metadata, but the actual content too.

Sounds awesome, right? 🚀

0 Comments

glass-bbok

No Comments Yet

Be the first to share your thoughts and start the conversation.

tick-guideNext Lesson

Congratulations on completing the Master Website Performance Optimization: Speed, UX, and Efficiency!