8 Tips for Debugging Next.js Applications

Tried and tested techniques from 4 years in the trenches

Published on
Dec 17, 2023

Read time
6 min read

Introduction

Next.js is the most popular JavaScript framework on the web right now. I have been working with Next.js for over four years, using the framework for both small personal projects and production applications — including my company’s main marketing site, which has several hundred pages and 10,000s of users per month.

There is a lot to commend about Next.js. In particular, we have benefited from the server-side functionality, which makes it straightforward to generate content during the build and keep our site SEO-friendly, while also enjoying the benefits of React.

But using the framework has also been frustrating at times. Our most tricky bugs have usually revolved around Next.js having a bit too much “magic”. Kent C. Dodds spoke about this in his article, Why I Won’t Use Next.js — and despite being a fan of the framework — I find myself agreeing with several of Kent’s criticisms. (In the interests of fairness, here’s a counterpiece by Lee Robinson: Why I’m Using Next.js.)

Sometimes, important Next.js functionality has felt obscure to me. Upgrading major versions has not always been painless. And, though the documentation is generally good, and almost always the best place to start, some important configurations or examples feel lacking — or they are not given the weight they deserve.

With that in mind, in this article I’ll share a handful of techniques that have helped me get to the bottom of some of the trickier bugs in my Next.js applications. These tools and techniques should save beginners time, but I’m hoping even experienced Next.js developers may learn something new!

1. Understand your build

Like many frameworks, Next.js behaves differently during development and when running from a build. On several occasions, this difference has led to issues that only occur in the built version of my app.

But understanding the build can be intimidating. By default, Next.js compresses and minifies your JavaScript code. This is good for production, but bad for debugging — but we can improve the situation!

In next.config.js, you can prevent compression by setting the compress property to false. Without compression, the file structure of your ubuild will much more closely resemble that of your source code.

module.exports = {
  // other configurations
  compress: false,
};

But the code will still be dense and unreadable! To un-minify this, you can edit the webpack configuration, usually found in the same file:

module.exports = {
  // other configurations
  webpack: (config) => {
    return {
      ...config,
      optimization: {
        ...config.optimization,
        minimize: false,
      },
    };
  },
};

Now, when you see the stack trace of a build-only error, you’ll be able to open up a given file and get a much clearer idea of what’s going on under-the-hood.

Just make sure not to deploy these changes, as they impact your runtime performance and Next.js warns that there’s even a risk of server-side-only code being downloaded by your users.

I recommend creating an environment variable such as DEBUG_MODE to update the config whenever you need:

module.exports = {
  // other configurations
  compress: process.env.DEBUG_MODE !== "true",
  webpack: (config) => {
    return {
      ...config,
      optimization: {
        ...config.optimization,
        minimize: process.env.DEBUG_MODE !== "true",
      },
    };
  },
};

In your package.json file, you could then have a build:debug script that runs DEBUG_MODE=true next build.

2. Run your app with NODE_ENV=development

A simple but easily overlooked tip for debugging is to ensure your NODE_ENV environment variable is set to development. This gives you much more useful logging, which you’ll miss out on otherwise!

For testing a build, you only need to provide the environment variable when you run next start, so this sequence of terminal commands would be fine:

yarn
yarn next build
NODE_ENV=development yarn next start

3. Check out your app without JavaScript

If you want to understand the HTML that gets generated at build time before any client-side JavaScript is run, such as when working on SEO, it could be useful to check out your app with JavaScript enabled.

How to do this depends on your browser, but in Chrome, you can do this by opening Settings, clicking Privacy and Security, clicking Site Settings, and then — under JavaScript — clicking “Don’t allow sites to use JavaScript”. Then run your built app with next start, open up the console and you can see the state of the DOM before any JavaScript has run.

Alternatively, you can find the initial HTML payload of your pages in the build folder: look for HTML files inside .next/server and you’ll see the initial state of the HTML.

4. Use a bundle analyzer

Next’s bundle analyzer has proved a very useful way to identify performance bottlenecks and get a clear picture of any large dependencies, assets or code files. It splits the bundle analysis into Next’s three main layers: the server, the client, and the edge runtime (a faster, less feature-rich runtime you can opt into, which is also used for middleware).

You can run the bundle analyzer dynamically via environment variables, like so:

// next.config.js
const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true",
});

module.exports = withBundleAnalyzer(myNextConfig);

We have used this to test the impact of introducing new libraries, to identify smaller alternative libraries (together with Bundlephobia) and — recently — to find a very large inline SVG, which ought to have been both compressed and served via a CDN!

5. Try hosting your app on Vercel

Even if you want to host your production app somewhere else, I have found Vercel’s hosting interface a very useful place to get a better understanding of Next.js. In particular, the Functions tab collects useful information on your API routes, including logs and errors, as well as information about usage, caching and performance.

When I was trying to get my head around Incremental Static Regeneration, I found it useful to see the individual function logs and how often a given request was being triggered.

Though a lot of this information will be available in other hosting platforms, Vercel’s tools are naturally tailored to Next.js, so you don’t have to spend time sorting through what’s useful and what isn’t. Vercel has a generous free tier, so if you don’t want to spend anything, you could just use Vercel to deploy a staging environment. Or try hosting hobby projects to get a better understanding of specific features.

6. Steer clear of a custom server

At work, the Next.js project I inherited used a custom server.js file. This was from a time when you couldn’t add a middleware.ts file and the options in next.config.js were much more limited. However, the custom server file has been the source of a lot of added complexity.

If you’re not 100% sure what you’re doing, you can easily end up interfering with desirable default functionality. For example, for a while, we ended up with both a server.js and a middleware.ts file handling different kinds of routes. Understanding the flow and hierarchy of our routing rules was much more complex than it needed to be.

The Next.js docs themselves warn against using custom servers:

“A custom server will remove important performance optimizations, like serverless functions and Automatic Static Optimization.”

“A custom server cannot be deployed on Vercel.”

Now we have the option of a middleware.ts file to handle custom routing logic. If you need something that can’t be achieved with middleware, you may be better off rolling your own separate server than trying to force Next.js into doing something it’s not optimized for — and creating trouble for yourself along the way.

7. Dive into the source code

When documentation isn’t enough, the best place to find answers is sometimes the source code. The Next.js codebase is available on GitHub. The codebase also contains a large collection of example apps.

The source code can be intimidating, but it is much less so if you are looking for something specific. We recently had a bug where Next.js’s header component wasn’t rendering meta tags that were part of child components. Looking at the source code of the Head component, we could see that it isn’t set up to handle nesting more than one layer deep.

8. Use React dev tools

Finally, Next.js is ultimately a React framework, and sometimes your Next.js bugs won’t be Next.js bugs at all. How to debug a React application could be the focus of its own article, but one of the best tools is undoubtedly the React Developer Tools Chrome extension. Being able to have a clear view of props, state and the hierarchy of components at any point is often a useful aid in a debugger’s toolkit.

For GraphQL users, the GraphQL Network Inspector extension also gets an honourable mention. For me, it’s a lot easier to read than the Network Panel in Chrome DevTools.

I hope you found this article a useful way to save time and headaches the next time you need to debug a Next.js application.

© 2024 Bret Cameron