Skip to content

What's new in Next.js 12

NextJS 12 came with a number of new features. Some are quiet and largely automatic, but others unlock completely new ways of working. I'll be giving you the highlights of each category, alongside ideas about how to leverage the new features and potential pitfalls.

In order of decreasing stability:

Stable: Performance improvements

In the category of features that have seen stable releases, require very few changes to your existing code, and can give you an immediate improvement we have a raft of performance optimizations for both user and developers:

Faster compiler

Next now includes a Rust-based compiler which it uses by default instead of Babel. This provides spectacular improvements in build times, especially for large projects, but it's worth noting that it does not include any plugin system, so if you are using a custom babel configuration, you will need to wait until the Next team adds support for what you need and keep using Babel until then. This de-optimization will happen automatically if you have a .babelrc file. At time of writing, for example,styled-components is now supported experimentally, but other systems like emotion or vanilla-extract aren't at all.

NextJS moving away from the Babel ecosystem, and towards a custom compiler, will likely mean decreased support for custom configurations going forward as well. In this environment, it seems advisable to stick as much as possible to the approaches officially recommended and supported by the NextJS team, like using CSS modules, SCSS, or styled-jsx for styling as opposed to a third-party CSS-in-JS solution. Trading flexibility for performance is sadly a common theme as software matures, but at least NextJS has a good variety of built-in tools to suit different situations.

Smaller images

NextJS can now generate AVIF images, which are smaller and thus faster to load than even WebP images. This optimization is opt-in, but there's very little downside to adding it to your next.config.js file. In the worst case, you might see slower build times if you have a lot of images.

module.exports = {
  images: {
    formats: ["image/avif", "image/webp"],
  },
};

Beta: Middleware

The most exciting change that you can actually use straight away — we'll get into the experimental React 18 features later — is page middleware. Middleware are functions that you can define, and which always run on the server right before a user enters any page in that folder or any subfolders.

It's a great way to enforce authentication by placing a middleware like this in an /app or /members folder where all registered-only content lives:

export async function middleware(req: NextRequest, res: NextResponse) {
  try {
    // check whether a user has a login token, like a JWT
    const userId = await verifyAndExtractToken(req.cookies[cookieName]);

    if (!userId) throw new Error("No userId encoded in token");

    // If they have a token the "next()" call delegates to the page to render
    return NextResponse.next();
  } catch (err) {
    // If they do not have a token we redirect them to login
    return NextResponse.redirect("/login");
  }
}

Middleware is somewhat limited by the fact it executes in an isolated context separate from the code that renders that page. It can read the request parameters like cookies and headers, and modify the response parameters in the same way but it cannot, for example, read some data and pass that on to a getServerSideProps function. Any augmentation of getServerSideProps will still require calling a function in the page file itself, although I hope in the future middleware gets some way of communicating with SSR, even if it is only through limited serializable values. It would be useful to be able to have getServerSideProps read the user id decoded from the token in the above example.

Still, a lot of common operations you need a server for are covered by reading and writing cookies and issuing redirects. A/B testing, page view and render time metrics, filtering out bots... the NextJS team have provided a whole raft of examples you can draw on for ideas.

Experimental: React 18

React 18 isn't even out yet, but NextJS 12 already includes support for it and all of its major features.

npm install react@alpha react-dom@alpha

If you install the React 18 like so, you can start testing out new features like update batching and startTransition, an API that lets you leverage async rendering to avoid blocking user interactions while UI updates render. However, the most transformational features, streaming render, and server components, are gated behind Next config flags:

module.exports = {
  experimental: {
    concurrentFeatures: true,
    serverComponents: true,
  },
};

There are full details of how to use these features, which promise a server-rendering experience that is faster and more seamless, in the NextJS docs. But keep in mind that React 18 is still in alpha, and these APIs might change before release, so it's not a good idea to restructure applications to rely on them yet. Couple that with the fact that many common React libraries you might be using or want to use won't be updated to be compatible with React 18 yet, and it's clear that this is something to just test out for now, and keep an eye on, in the coming months as final APIs and releases begin to appear.

Experimental: URL imports

In the final experimental category, we have an idea that might be familiar to those of us who've used Deno: importing dependencies not by adding them to a package.json, but by including their URL in code. It's better to still structure your code so that a given URL is only imported in one place, so that becomes the canonical version used throughout your app, and which you can update by changing its URL. This usually takes the form of a central dependencies file, or set of files with declarations like the following:

export { default as padLeft } from "https://cdn.jsdelivr.net/npm/pad-left@2.1";

You can enable this feature by listing the host names from which you want to import dependencies in Next config:

module.exports = {
  experimental: {
    urlImports: ["https://cdn.jsdelivr.net"],
  },
};

This approach can have certain advantages over the standard central package.json model:

  • You don't get errors when you pull code that adds dependencies but don't know to run an install.
  • You can divide up dependencies as you wish. For example, if different sections of the site are being developed separately they can each have their own dependencies file.
  • If some parts of the application depend on an old version of a package, you can still use the newer version for new code until you get around to tackling the tech debt.

However, many packages and tools don't yet work well under this model, with the concept of peer dependencies being particularly difficult to translate although CDNs like ESM have some ideas to get them working.

Conclusion

It's good to see NextJS get ahead of the React 18 and continue making performance improvements, but it doesn't look like this update contains anything truly life-changing or paradigm-shifting yet. However, this is a clear sign that React's streaming and async renderer is going to be a reality very soon, so if you are not already familiar with the concepts or your app doesn't quite run in Strict Mode, it's a good time to learn and prepare.