DEV Community

Peter Jacxsens
Peter Jacxsens

Posted on

Cache components and cache persistence in NextJs

Take a look at this sayHello example from an earlier chapter:

Note: the examples are available on GitHub.

// app\lib\sayHello.ts

export async function sayHello(name: string) {
  'use cache';
  console.log('Running sayHello with arg ', name);
  return `Hello, ${name}`;
}
Enter fullscreen mode Exit fullscreen mode

We used it in a <Hello /> component:

export async function Hello({ name }: { name: string }) {
  const res = await sayHello(name);
  return <div>{res}</div>;
}
Enter fullscreen mode Exit fullscreen mode

And loaded it in a route:

// app\chapter-13\hello\page.tsx

export default function HelloPage() {
  return (
    <>
      <Hello name='Bob' />
      <Hello name='Bob' />
      <Hello name='Fred' />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

This proved caching components works because at build time it only logged Running sayHello with arg Bob once. The second time the function was called, it hit he cache.

We create a new route: /chapter-16/hello-dynamic:

// app\chapter-16\hello-dynamic\page.tsx

async function Hellos() {
  await connection();
  return (
    <>
      <Hello name='Bob' />
      <Hello name='Bob' />
      <Hello name='Fred' />
      <Hello name='Mike' />
    </>
  );
}

export default function Page() {
  return (
    <Suspense fallback='loading Hellos'>
      <Hellos />
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

We use connection to force dynamic rendering of <Hellos />. We nicely wrapped it inside a Suspense boundary. This means that <Hellos /> is not prerendered in the static shell. At request time - when the route chapter-16/hello-dynamic is visited - <Hellos /> will be rendered server-side and then streamed in. We run build:

Running sayHello with arg  Bob
Running sayHello with arg  Fred

...

├ ○ /chapter-13/hello
...
├ ◐ /chapter-16/hello-dynamic

○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content
Enter fullscreen mode Exit fullscreen mode

We get logs that sayhello ran with "Bob" and "Fred". But that's from our other route: /chapter-13/hello. The absence of a log for "Mike" confirms that <Hellos /> inside /chapter-16/hello-dynamic was deferred to runtime PPR streaming rather than prerendering.

Great, everything is as expected. Now we actually run the app in production mode: next start and we visit /chapter-13/hello (the not dynamic route).

example

What happens? Well, nothing. The route was prerendered and prerendered .rsc was used to update the route. No logs in the terminal. This is 100% what we expected. We are served prerendered content.

Next, we visit /chapter-16/hello-dynamic:

dynaic example

And we get this in our terminal:

✓ Ready in 149ms
Running sayHello with arg  Bob
Running sayHello with arg  Fred
Running sayHello with arg  Mike
Enter fullscreen mode Exit fullscreen mode

So, what is this?

  1. Obviously <Hello /> rendered server-side. I wasn't prerendered so it rendered server-side.
  2. It cached sayHello('Bob') because that only logged once. The second time sayHello('Bob') ran, it was retrieved from cache.

But sayHello('Bob') and sayHello('Fred') ran once and this is kind of unexpected. Both were prerendered in the chapter-13/hello route, so why weren't they retrieved from cache?

The answer is simple. The cache did not persist between the build environment and the run environment. This is normal behavior:

  • We ran next build. This cached "Bob" and "Fred". Once the build was complete, the server (that runs the build) stopped and the cache (in memory) was lost.
  • We ran next start. This started up a new server. When we visited the route /chapter-16/hello-dynamic for the first time, <Hellos /> ran and called sayHello 4 times:
    1. sayHello("Bob"): no cache hit, run function, cache result.
    2. sayHello("Bob"): cache hit, return cache
    3. sayHello("Fred"): no cache hit, run function, cache result.
    4. sayHello("Mike"): no cache hit, run function, cache result.

This accounts for the three logs after the request.

Server side, runtime cache

Now that we cached these sayHello functions, they won't run again (unless they expire, see later). When we leave the route and revisit it, the log stays empty. When we close down the browser and reopen the route, the log stays empty. When we visit in another browser, the log stays empty. So, every visitor gets served from the same server-side cache. This cache was created when we first visited /chapter-16/hello-dynamic.

Cache not persisting

The takeaway here is that cache does not always persist. This can lead to unexpected behavior. These are some instances when cache doesn't persist: (Gemini helped me write this)

  • When you run a new build.
  • When you make a new deploy.
  • Serverless: new container or cold start.
  • Memory limits reached.
  • When running Draftmode.

Cache not persisting is fine in most cases. When it's not, you have to look into solutions using "use cache:remote". This is beyond the scope of this series.

If you want to support my writing, you can donate with paypal.

Top comments (0)