DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Five flags in next.config.mjs, and the one whose only job is to make a response header bigger

The config file for pub-trivia.app is short, and almost all of it is performance. Next 16.2, React 19.2. Five of the settings are worth explaining, because for three of them the default is fine for most projects and for two of them the default was costing us something specific.

reactCompiler: true

reactCompiler: true,
Enter fullscreen mode Exit fullscreen mode

One line, plus babel-plugin-react-compiler in dev dependencies. The compiler works out which values in a component actually need memoising and does it at build time, which means useMemo, useCallback and memo stop being things you hand place.

What sold it was not speed, it was the diff I did not have to write. The pages that take a quiz pack and render an editor with a list of questions in it were the usual accumulation of useCallback wrappers added one at a time, each one because something re-rendered more than it should. None of them were interesting and all of them were load bearing in a way nobody could verify. Turning the compiler on lets that category of code be deleted rather than maintained.

The honest caveat: it only helps code that follows the rules of React, and the compiler bails on components it cannot prove things about rather than miscompiling them. If your components mutate props or read refs during render, you get a quiet no-op rather than a win. That makes it a cheap flag to try and a hard one to measure, which is an uncomfortable combination if you want a number. I would still take it over hand placed memoisation.

staleTimes, which is the one that changes behaviour you can feel

staleTimes: {
  dynamic: 30,
  static: 180,
},
Enter fullscreen mode Exit fullscreen mode

This is the Next client side router cache: how long an already visited route stays usable without going back to the server when the user navigates to it again. The default is aggressive about freshness, which is the right default for a dashboard showing live data and the wrong one for a marketing site where somebody is clicking between features, pricing and the guides comparing them.

Three minutes on static pages is a judgement about content, not about caching: those pages change when I deploy, and nobody reading them is harmed by a copy that is up to three minutes old. Thirty seconds on dynamic ones is the same judgement made nervously, because a dynamic page in this app is usually something with the user's own data on it, and a user who edits something and navigates back to a stale copy of it will believe the edit was lost.

If you set this, set the dynamic value by asking what the worst stale thing a user could see is, not by picking a number that feels fast.

optimizePackageImports, 11 entries

optimizePackageImports: [
  'lucide-react',
  '@radix-ui/react-dialog',
  // ...
  'react-icons',
  'framer-motion',
],
Enter fullscreen mode Exit fullscreen mode

Barrel files are the problem being solved. You import three icons from lucide-react and the bundler has to reason about a module that re-exports thousands of them. This flag tells Next to rewrite those imports to the individual modules, so the work of proving the rest is unused never has to happen.

The two entries that matter most in this list are the two icon packages. Everything else is a Radix primitive, and those are small individually, but they are imported from a shared UI directory that almost every page touches, so they are in the dependency graph of nearly every route.

Worth knowing: Next already applies this to a list of popular packages by default, so some of these entries may be belt and braces. I keep them listed explicitly anyway, because the default list is a detail of the framework version and this file is where somebody will look to find out why a build behaves the way it does.

reactMaxHeadersLength: 8000

reactMaxHeadersLength: 8000,
Enter fullscreen mode Exit fullscreen mode

This is the one that reads like nonsense until you see what it controls. React can emit resource hints as HTTP Link headers during streaming, so the browser starts fetching a font or a stylesheet before it has parsed a single byte of the HTML body. There is a cap on how many bytes of headers it will emit, and anything past the cap is dropped silently.

You can see the result on the live site:

curl -sI https://pub-trivia.app/ | grep -i '^link'
Enter fullscreen mode Exit fullscreen mode

The response carries a Link header with the two self hosted woff2 font files in it, each marked rel=preload; as="font"; crossorigin. The browser can start those two requests during the time it would otherwise be waiting for the <head> to arrive. Fonts are the right thing to spend that budget on, because a late font is the most visible kind of late: text that reflows or flashes after the user has already started reading.

Raising the cap does not create hints, it only stops them being truncated. If you have a lot of them and you have never looked at this header, some of yours may already be missing.

The two one word ones

compress: true,
poweredByHeader: false,
Enter fullscreen mode Exit fullscreen mode

Compression is on, and verifiable:

curl -sI -H 'Accept-Encoding: gzip, br' https://pub-trivia.app/pricing | grep -i content-encoding
Enter fullscreen mode Exit fullscreen mode
content-encoding: br
Enter fullscreen mode Exit fullscreen mode

Brotli, chosen by the platform. The pricing page is mostly text and markup, which compresses extremely well, so this is the single highest leverage byte for byte setting in the file.

poweredByHeader: false removes X-Powered-By: Next.js. Calling it security is overselling it, since the framework is obvious from the URL structure of the assets, but there is no reason to announce a version number to everybody who runs curl -I and it is one word.

What the flags do not do

Worth saying, because a config file full of performance settings can make you feel finished. Every setting above is about how code is bundled and how responses are cached. None of them touch what you ship. The largest performance problem this site has had was two 1.8 MB images that nothing in this file could have helped with, because the problem was the images, and the fix was in the component that referenced them.

If you want the observable version of that lesson rather than the config version, open the landing page with the network tab filtered to images: the hero image comes through the optimiser at around 10 KB, and the decorative ones below it are a different story entirely, for a reason that lives in the CSS rather than in next.config.mjs.

curl -sI https://pub-trivia.app/ | grep -iE 'strict-transport|x-powered-by'
Enter fullscreen mode Exit fullscreen mode

One line comes back, not two. The config decides what is in that response; the pages decide how big the rest of it is.

Top comments (0)