On caching in Next.js App Router

4 min read
This post does not explain caching in Next.js.
Post cover imageImage by black-forest-labs/flux-dev

Caching. A lot of thinking goes into caching in App Router. Some of the decisions around caching were contentious, specifically the patching of fetch.

By default, Next.js automatically caches the returned values of fetch in the Data Cache on the server. This means that the data can be fetched at build time or request time, cached, and reused on each data request.

I can't say I disagree. I don't see why we couldn't have a next/fetch export. We already have next/link, next/image, next/script. There's also next/form but it hasn't been released at the time of writing.

Anyway, I put this behind me and moved on. Can't let things out of your control bother you too much. And who knows, maybe there's something I don't know, so I give the benefit of the doubt. I'm sure the Next.js team gave this much thought.

After using the App Router in production, my feelings were that the caching behavior was overly aggressive. Other engineers I complained talked to felt the same. We ran into a few gotchas and we ended up trying to opt-out of caching whereever possible – we simply didn't need it for the type of app we needed to build. We had gotten used to how React made all memoization opt-in and App Router felt like a deviation from that model.

I think this was heard by the Next team. The next major release of Next (ha, I wonder what lingo they use internally) is v15.0 and it makes some very welcome tweaks to caching.

I recently refactored this site from the Pages Router to the App Router. Getting a better understanding of caching paradigms was at the top of my list for 'why refactor'. While I've gone through the caching docs more than once, some concepts just never clicked. I felt that the page was overwhelming. I would read it and leave with "ideas" about how caching worked, never feeling sure I actually understood. The refactor won't give me a complete understanding, but it would be a practical lesson on what's most important.

Once I explored this in practice, everything became a lot clearer. The docs made a lot more sense. And I started to enjoy using Next.js a lot more. I have to say, I have more empathy now for whomever wrote that page. As you know, concepts in software become more difficult to explain the more experience you have with them, and I imagine the author was breathing Next.js every day.

So why does caching in App Router seem complex? Well, because it's doing a lot for us.

As a developer, I want to be in control. I want to be sure of how my code works. I'm very confident in my understanding of how a regular React app runs. But App Router? That's new. There are lots of abstractions. I'm not sure how the app works anymore. I'm not certain. I need to learn.

That's where the feeling of frustration comes from.

But, why would the framework do this? Why would it frustrate us by adding so many abstractions?

Because it's a framework. It's in the name. That's its job.

As a framework user, you want the framework to use your code to make you productive. It should infer requirements not explicitly stated by your code and the app should behave how most users would expect. You shouldn't have to refactor your app later on just because you're moving from static to dynamic rendering and vice-versa. You shouldn't need deep familiarity with the inner workings of the framework to write an app that works great.

Let's be fair. Those are challenging requirements.

A server component is static by default. You prerender it from React to HTML at build time. When receiving a request, respond with the HTML. Now change it to access a cookie from the user's request. It dynamically renders each request. Fetch from an API in the render function. Fetch from the same endpoint further down the tree. The fetch call gets automatically memoized so a render results in a single call.

We weren't explicit about how this should happen, and yet it did.