2024-03-15

Efficiency in Next.js static builds

Today was not a busy day at work, which meant that I got to do one of my favorite thingsā€”think very deeply about a very boring problem.

The problem is boring because it's extremely common: builds take too long, you hit your concurrency limit, and before you know it you're waiting an hour for your latest commit to even start building. The case I worked on today made for a particularly frustrating developer experience. The site was content-heavy, and all ~5000 pages were building on every push.

That's right, they were running 15 minute builds on every push. Fortunately, there are a couple simple ways to mitigate this.

Render a subset of pages at build time

If you have a site with thousands of pages that should be rendered as static pages, you probably don't want to do this all at once. If those pages all depend on external data from, say, a CMS, you definitely don't want to do this all at once. This isn't my idea; the Next.js docs on fallback pages will tell you the same thing.

What's interesting is which pages you choose to render. Do you choose the top 10% in terms of traffic? This makes sense, but you need to be able to fetch traffic data. Do you choose a static list of pages that you know are frequently accessed? This also makes sense, but who is responsible for maintaining that list? How often should it be updated?

Any data-driven approach adds complexity to what should be a simple problem. And in this case, complexity adds time to a long process you are explicitly trying to shorten.

Statically render for production only

There is a tradeoff here, but it's a pretty sensible one. Production builds happen less frequently than preview builds, and so they spend less time blocking in total, even though they still take a long time. In exchange for not statically rendering previews, however, the dev experience can be slower because every page becomes fully dynamic (at least for the first request).

This might be a problem on extremely complex pages, but if you know ahead of time that those pages will take a long time to render, you can render them ahead of time. But is that actually an improvement? How can you make a general decision about preview builds that will vary, often wildly, from one commit to the next?

The waiting game

The way I evaluate this problem is to ask: who is waiting, and for how long? Once you can answer those questions, you can begin to think about the implications.

In many cases, we think about static rendering as a benefit for the end user. Which it is! Static sites are faster, simpler, and when your website is a major revenue channel, that part of the user experience matters a lot.

End users are not just the people looking at production though. Developers checking their builds are end users of the hosting product, and the more time they spend waiting on builds, the higher the chance they grow dissatisfied with the developer experience. Can you blame them?

Once you know who is waiting and for how long, you can start to optimize. Maybe you render 20% of pages when building previews, and 100% for production. Then you shift toward a stronger culture of continuous delivery, and maybe you render only 75% of production pages so that you can ship several times a day. It's a constant game of finding the perfect balance, but getting from "blocked" to "unblocked" at a basic level is not very hard at all.

Static is a spectrum

The solutions I discussed above are not mutually exclusive. The idea of a "static" build is not as black and white as it used to be, either. There's a range of ways to solve this problem, and when you look closely, it's not actually a boring problem at all. It's a system waiting to be designed.

There is a clear financial impact here, by the way. Developer experience is important because people shouldn't have to be miserable, but it's more than feel-good sentiment. The customer experiencing this problem was thinking about dramatically increasing their concurrency limits, which would have been a fairly steep increase in cost.

The lesson here is about recognizing tradeoffs. Static sites are supposed to be šŸ”„Blazing FastšŸ”„ so why wouldn't you render everything statically? Well, like most things in our industry, it's not quite that simple.