Most developers think their app is slow because of the backend.
In reality, a lot of apps feel slow because of how we render them.
You’ve seen it:
- Blank screen.
- Then everything appears at once.
- Or worse — layout shifts everywhere.
That’s not always a data problem.
That’s a rendering strategy problem.
With the App Router in Next.js, Streaming and Suspense fundamentally change how users experience loading. And once you understand it properly, you’ll never build dashboards the old way again.
Let’s break it down properly.
The Real Problem: Traditional Rendering Blocks Everything
In classic SSR:
- Server fetches all data.
- Waits for everything to resolve.
- Generates full HTML.
- Sends it to the client.
If one API call is slow, the whole page waits.
Even if 80% of the UI is ready.
That’s inefficient.
Users don’t need everything at once.
They need something fast.
That’s where streaming comes in.
What is Streaming in Next.js?
Streaming allows the server to send HTML to the browser in chunks instead of waiting for the entire page to be ready.
Instead of:
Wait → Render → Send
It becomes:
Render what’s ready → Send → Continue rendering → Stream more
This is supported natively in the App Router.
The browser starts rendering immediately, and slower parts of the UI are filled in later.
Perceived performance improves dramatically.
And perceived performance is what users care about.
Where Suspense Comes In
Streaming alone isn’t enough.
You need boundaries.
That’s where React Suspense comes in.
Suspense lets you define which parts of the UI can “wait” independently.
Example:
import { Suspense } from "react";
import SlowComponent from "./SlowComponent";
import FastComponent from "./FastComponent";
export default function Page() {
return (
<div>
<FastComponent />
<Suspense fallback={<p>Loading analytics...</p>}>
<SlowComponent />
</Suspense>
</div>
);
}
Here’s what happens:
FastComponent renders immediately.
SlowComponent fetches data.
While it loads, fallback UI is shown.
When ready, it streams into place.
The rest of the page does not wait.
That’s powerful.
Real-World Example: Dashboard Scenario
Imagine a dashboard page:
- User profile → fast query
- Notifications → medium query
- Analytics chart → heavy aggregation
Without streaming:
👉 Everything waits for analytics.
With streaming + Suspense:
- Profile renders instantly.
- Notifications appear shortly after.
- Chart loads last with a proper fallback.
- User feels speed.
Even if total load time is identical.
That’s the difference between technical performance and perceived performance.
How Next.js Makes This Even Better
In the App Router:
- Server Components are streamed by default.
- Each route segment can have a loading.js.
- Suspense boundaries control how chunks are streamed.
- No extra configuration required.
Example:
app/dashboard/loading.js
This automatically acts as a Suspense fallback for that route segment.
You don’t have to manually wire everything.
Important: Streaming is a Server-Side Concept
This is where many developers get confused.
Streaming happens during server rendering.
It is not the same as client-side lazy loading.
Suspense inside Client Components behaves differently than in Server Components.
If you use "use client" everywhere, you reduce the benefits.
Let the server do the heavy lifting whenever possible.
Common Mistakes I See
❌ Wrapping the entire page in one Suspense
This defeats the purpose. Everything still waits together.
❌ Adding too many Suspense boundaries
Over-fragmenting UI creates janky loading experiences.
❌ Ignoring fallback UX
A spinner is not always the best fallback. Skeletons often feel better.
❌ Thinking Streaming replaces caching
It doesn’t. Caching and streaming solve different problems.
Why This Actually Matters
Most developers optimize for Lighthouse scores.
But users don’t see metrics.
They feel:
- How fast something appears.
- How stable the layout is.
- How interactive the page feels.
- Streaming + Suspense directly improves that perception.
And that’s what good frontend engineering is about.
Not just shipping components.
Designing experience.
Final Thoughts
When I first used Streaming in Next.js, I realized something:
We’ve been thinking about rendering too rigidly.
Pages don’t need to load like monoliths.
They can load like conversations — piece by piece.
The App Router isn’t just a routing update.
It’s a mental model shift.
If you’re still building pages that wait for everything before showing anything, you’re leaving performance (and UX) on the table.
Start thinking in boundaries.
Start thinking in chunks.
And let the server stream.
If you found this useful, I’d love to hear how you’re using Suspense in your projects.
Top comments (5)
I’ve been using Next.js for my projects, but I’ll admit I have never personally used Suspense yet. I think I’ve been too focused on the traditional SSR flow fetching everything and just hoping the API is fast enough. I'm definitely going to try implementing some loading boundaries in my next build. Great read :)
That’s completely fair, I think most of us start with traditional SSR and just optimize the API instead of the rendering strategy.
Suspense can feel unnecessary until you try it once in a real UI. The moment you see part of your page render instantly while a heavier section streams in, it kind of clicks.
If you’re experimenting, I’d suggest starting small — maybe wrap a slower analytics widget or recommendation section instead of the whole page. That’s usually where the benefit becomes obvious.
Curious to hear how it feels once you try it in your next build 🙂
Wiill definitely give my thoughts when I try it
The "pages don't need to load like monoliths, they can load like conversations" line is a really nice way to put it.
One thing that tripped me up when I first started using Suspense boundaries in production: figuring out the right granularity. Too few boundaries and you're back to waterfall behavior. Too many and you get this weird popcorn effect where skeleton loaders are flickering in and out all over the page — arguably worse UX than just waiting.
What helped me was thinking about it from the user's perspective: what do they actually look at first? Put your Suspense boundaries around what they DON'T look at immediately. The hero content should be synchronous, the sidebar analytics or recommendation widgets can stream in.
Also worth noting — if you're using
loading.jsfiles, they apply to the entire route segment. For more fine-grained control within a single page, you'll still want inline<Suspense>boundaries wrapping specific async server components. The two approaches complement each other nicely.That’s such a good point about granularity.
The “popcorn effect” is real, I’ve seen dashboards where everything streams independently and the UI feels unstable instead of fast.
I like how you framed it around user attention. Designing Suspense boundaries based on what users look at first is a much better heuristic than just wrapping every async component.
And yes — the distinction between
loading.jsat the route segment level vs inline<Suspense>for finer control is important. I think a lot of people assume one replaces the other, when in reality they solve different layers of the same problem.Appreciate you sharing that — this is exactly the kind of nuance that only shows up once you use it in production.