Next.js
Performance
Cache
Arquitetura
React

Caching and streaming in Next.js: performance became an architectural decision

Perceived performance is no longer a final adjustment and has become an architectural choice, with real gains and the risk of old data.

Caching and streaming in Next.js: performance became an architectural decision

For a long time, performance was the last stage of the project. The application was built, measured at the end, and when the page was slow, palliatives came out: a cache here, a spinner there, a lazy load there. It was a finishing detail, handled after the important decisions had already been made.

Modern Next.js dismantles this order. Cache, streaming, and Suspense are not buttons you turn on at the end; they are properties of how the page is assembled and delivered. Deciding when data is recalculated, in what order the parts of the screen appear, and what can be served before the rest are structural choices. The thesis of this text is simple: perceived performance became an architectural decision, and treating it as a final detail became costly.

Three mechanisms that solve different problems

It's worth separating what each piece does, because they tend to be confused with a single vague idea of "leaving quickly".

Cache is about not redoing work. If data has already been fetched or a page has already been rendered, saving it avoids paying the same cost again on the next request. The gain is in throughput and latency: responses that do not need to touch the bank are output in fractions of the time.

Streaming is about not waiting for everything to be ready to start delivering. Instead of holding the entire page until the slowest part finishes, the server sends the HTML in chunks as each becomes available. The user begins to see and interact with what has already arrived while the rest is still being assembled.

Suspense is what makes streaming usable. It allows you to declare, in the component itself, "while this data is not enough, show this". It marks the boundaries between what is ready and what is still loading, giving the framework permission to send the screen in coherent chunks instead of a single block.

How they work together

The magic appears in the combination. Imagine a product page: header, item data, reviews and recommendations. The header and item data are fast. Assessments rely on heavy aggregation. Recommendations call for a slow external service.

In the old model, the entire page waited for the slowest component. The user looked at a blank screen until everything, including the recommendation that came from a grumpy third party, was finished. The page's performance was hostage to its worst element.

With Suspense delimiting each block and active streaming, the server instantly delivers the item header and data, with loading indicators in place of ratings and recommendations. As each part is ready on the server, it is transmitted and clicks into place. Underneath, the cache ensures that on the next visit, the parts that haven't changed don't even need to be recalculated. The three mechanisms add up: caching reduces work, streaming removes the worst-case wait, Suspense organizes delivery.

The result is that perceived performance stops depending on the slowest component and starts depending on how you drew the boundaries. And drawing borders is architecture.

Why this becomes an architectural decision, not a final adjustment

Note that each choice above was made early, not at the end. Where to place a Suspense limit, what data can wait and what needs to be in the first byte, what is cacheable and for how long: all of this shapes the component structure and the way data is fetched.

You can't "add streaming later" on a page that was written as a monolithic block that fetches everything at once. To stream, the page must have been designed in independent parts, each with its own loading boundary. This decomposition is a design decision that happens at the beginning, along with data modeling.

The same goes for the cache. Deciding what can be served from a saved version and what needs to be always fresh is, in practice, classifying your domain's data by outdated tolerance. This isn't a performance tweak, it's a statement about the business: could this price be a few minutes old? This balance, no? These answers belong to the architecture, and those who put them off until the end discover that rewriting the data search structure is much more expensive than having thought of it before. The relationship with the rest of the framework is clearer in the Next.js App Router guide.

The risk that no one mentions in the beautiful slide: misunderstanding cache

Cache is the most seductive and the most dangerous part. The classic phrase that cache invalidation is one of the difficult problems in computing is not a programmer's joke, it is a production description.

The central problem is old data. The moment you decide to save an answer, you accept that it may be out of date when someone reads it again. For content that changes slowly, great. For a balance, a stock, an order status, serving a version that has been saved for too long means showing the user a reality that no longer exists. And the worst type of this bug is the silent one: nothing breaks, nothing gives an error, the screen simply lies.

Next.js offers fine caching controls, precisely because these decisions need to be per-data, not global. But fine control is a double-edged sword: the developer who does not understand exactly in which layer data is being stored will debug phantom behaviors. Why doesn't this page update? Because there is a cache layer that he forgot existed, with an invalidation key that no one triggered. Anyone who wants to delve deeper will find a good overview in good caching practices in applications.

Complexity is part of the deal

There is a cognitive cost that needs to be honestly included. With multiple layers of caching, streaming, and Suspense boundaries, the mental model of "what's happening when the user requests this page" becomes richer, and harder to hold in your head.

Data can be stored at more than one level, with different lifetimes. One part of the page renders on the server and streams, another hydrates on the client. When something appears outdated, the investigation needs to go through these layers to find out where the old version got stuck. This requires the team to understand the model, and not just copy configurations from an example on the internet.

Therefore, the practical recommendation is to be explicit and conservative. Start with less cache than seems tempting and add layers as measurement warrants, documenting the invalidation strategy for each. Treat aggressive caching on sensitive data as a decision that needs justification, not as a default. The golden rule is that no one should be able to explain why a screen shows old information just by shrugging their shoulders.

What to take into the decision

Cache, streaming and Suspense, together, made the perceived performance much better than was possible with the finishing tricks of the previous generation. The gain is concrete: pages that appear in parts, work that is not repeated, waits that do not slow the user down in the worst case.

The counterpoint is that these gains come from decisions made early, about component boundaries and tolerance for outdated data. Postponing them until the end is not neutral: it is giving up streaming and pushing the cache into the territory of silent old data. Perceived performance became architecture, with all that this implies of planning and invalidation discipline.

If you are defining the stack of an application Next.js and want to avoid the cache nightmare that no one understands, it is worth designing the data strategy before the first screen. I have discussed this design a lot; Find me in the comments or on the networks to exchange ideas.

Also read