Increasing the Performance of Dynamic Next.JS Websites

by Guido Maliandi on November 6th, 2017

Increasing the Performance of Dynamic Next.JS Websites cover

One of the most obvious signs that a site is made with Next.JS is that you load the page, you click a link to another section and it loads instantly. That’s because when a page is loaded, Next.JS will download in the background all the other pages linked with tags. This works great for static pages, but as soon as you have to fetch some data in the getInitialProps, loading that page will take time even if it the page code is prefetched. And what is worse — unless you explicitly add a loading indicator, it will usually take time with no indication in the UI! Prefetching and caching data is not trivial — for example, it could be wrong to serve a slightly outdated version of certain information. Also, different data could have different caching rules. Here at Scale we decided to implement a lightweight caching and data prefetching strategy, which allows having this instant navigation experience without compromising data freshness.

Caching in getInitialProps

On certain parts of our site, we know it is OK to show data that is a few minutes old. For those cases, we implemented a simple caching strategy that stores data with a 5 minute TTL. The implementation uses an abstraction around the fetch api that we called cached-json-fetch:

1import lscache from 'lscache';
2import fetch from 'isomorphic-fetch';
3const TTL_MINUTES = 5;
4export default async function(url, options) {
5// We don't cache anything when server-side rendering.
6// That way if users refresh the page they always get fresh data.
7if (typeof window === 'undefined') {
8return fetch(url, options).then(response => response.json());
10let cachedResponse = lscache.get(url);
11// If there is no cached response,
12// do the actual call and store the response
13if (cachedResponse === null) {
14cachedResponse = await fetch(url, options)
15.then(response => response.json());
16lscache.set(url, cachedResponse, TTL_MINUTES);
18return cachedResponse;
20export function overrideCache(key, val) {
21lscache.set(key, val, TTL_MINUTES);

We then use this in the getInitialProps of our pages:

1import React from 'react';
2import cachedFetch, { overrideCache } from 'lib/cached-json-fetch';
3const SOME_DATA_URL = '/some_data';
4export default class SomePage extends React.Component {
5static async getInitialProps(ctx) {
6const someData = await cachedFetch(SOME_DATA_URL);
7const isServerRendered = !!ctx.req;
8return { someData, isServerRendered };
10componentDidMount() {
11// When the page is server-rendered,
12// we override the value in the client cache
13if (this.props.isServerRendered) {
14overrideCache(SOME_DATA_URL, this.props.someData);

Now, when we client-navigate to this page, it will check the cache first before fetching the data from the server. Alternatively, on a full page reload, it will always fetch the latest data from the server and reset the TTL.

Prefetching data

With a caching layer like this in place, it is extremely straightforward to add prefetching. In the example above, all we need to do is call getInitialProps so the cache is populated with all the necessary data to load the page. Now, if this page is client-navigated to before the TTL expires, it will load instantly just like a static page! To achieve this, we can create a simple abstraction over Link, which not only downloads the page structure in the background but also calls its getInitialProps to populate the cache:

1import React from 'react';
2import Link from 'next/link';
3import { resolve, parse } from 'url';
4import Router from 'next/router';
5export default class DataPrefetchLink extends Link {
6async prefetch() {
7if (typeof window === 'undefined') {
10const { pathname } = window.location;
11const href = resolve(pathname, this.props.href);
12const { query } = parse(this.props.href, true);
13const Component = await Router.prefetch(href);
14if (this.props.withData && Component) {
15const ctx = {pathname: href, query, isVirtualCall: true};
16await Component.getInitialProps(ctx);

Using this in our pages is as simple as: <Link prefetch withData href="…">. In case you have some calls in your page’s getInitialProps that do not use a cache, you can use the isVirtualCall flag in the context to avoid making them when the method is called for caching purposes only. For example:

1import React from 'react';
2import cachedFetch, { overrideCache } from 'lib/cached-json-fetch';
3import fetch from 'isomorphic-fetch';
4const SOME_DATA_URL = '/some_data';
5export default class SomePage extends React.Component {
6static async getInitialProps(ctx) {
7const someData = await cachedFetch(SOME_DATA_URL);
8const isServerRendered = !!ctx.req;
9const isVirtualCall = ctx.isVirtualCall;
10// No need to call this when prefetching the page,
11// since this data won’t be cached
12let someNonCachedData;
13if (!isVirtualCall) {
14someNonCachedData = await fetch('/some_non_cached_data')
15.then(response => response.json());
17return { someData, someNonCachedData, isServerRendered };
19componentDidMount() {
20// When the page is server-rendered,
21// we override the value in the client cache
22if (this.props.isServerRendered) {
23overrideCache(SOME_DATA_URL, this.props.someData);

The key element that makes this possible is that the programmatic prefetch API through Router.prefetch returns the page’s constructor, so we can call the getInitialProps directly on that object! You can find this extended Link component in npm as data-prefetch-link. In conclusion, we learned that with some fine-tuning we can make our dynamic next.js pages load as fast as static pages, without sacrificing the freshness of the data or the flexibility to choose which data we want to cache. We hope this helps you make your web platforms faster!