How to cache all pages in Next.js at server side

Next.js is a great SSR framework to create apps and websites. SSR means that you can use React.js code at server and client can access any url or location any time and will get rendered HTML just like it would be rendered in the browser.

SSR + React = slow SSR

The problem is that React is pretty costly when it is about rendering the page at the server side. It’s OK for humans if the page is rendered in 0.1s — in a blink of an eye. But for server that time is too big and means low throughput. Google PageSpeed Insights asks you to reduce server response time and your website score is lower if page rendering is longer than 0.2s-0.25s.

The solution is to cache pages. You can always use nginx to cache everything. Ngninx is good in caching. You just put it between clients and your app. But nginx caching rules may be not enough flexible for your use cases.

Caching inside your app gives you much more control. There is a convenient way to do that in next.js project using express.js custom server. In that custom server you can let your app decide should it render a page, return cached content or even which exact version of it.

Next.js repo at Github has a source code of the example app where it caches SSR’ed pages in the memory. But it caches only one location of many and doesn't work if you try cache all pages. That happens because a special location /_next/ is used to serve static content and requires different processing. Using this example I created a code that allows to cache all pages and serve static content properly.

One server.js to cache them all

What exactly happens here?

First of all I create an express instance that will handle requests instead next.js.

Also I need next.js instance to do all the work except caching. It is available in the app constant.

Then I use LRUcache instance to store the cache. You can use any other storage if you want. Remember that LRUCache setting max has low meaning on its own an requires length function to be defined to provide you better control on cache size. Property maxAge allows you to define the time when the entry expires anyway.

The const handle is the default next.js handler for requests and will be used to serve static content.

Requests with urls starting with /_next/ have to be processed with next.js itself. I create route /_next/* that transfers them to the default next.js handler functionhandle and now will be processed correctly.

All the other requests will go to server.get('*') and will be cached after being rendered. You can define other locations to handle different situations or even use different storages. I don’t need it, so lets dive into the renderAndCache function mechanics.

Its code is almost the same as it is in the example in next.js repo. It has x-cache header, but I’m not sure if it has any other meaning except just to allow you to see in the browser or other client software was the url served from cache or rendered.

You don’t need to change the code of that function, because the control of what will be server from cache or will be rendered has to be done by only changing the key generated for that request.

Key-value storage has a simple idea — you put something with a name to the storage. And when you need it, you just ask the storage to give you what ever it has with that name. If it gives you something — you use it. If it gives you nothing — you have to create it.

I use req.path as a key. It is generated in the function getCacheKey() which has only one line that just returns one variable. But why use a function for such a simple task? Because this function allows you use all the information provided with request to create a key that is build from all parameters that affect the rendering.

Next time I’ll show you how to make simple redirects with next.js.