Mastering Next.js 14: Static and Dynamic Rendering with Streaming
Tags: webdev, nextjs, javascript, beginners
Published: 2024-03-24 04:29:54

In this chapter
Here are the topics we’ll cover:

⚡️ What static rendering is and how it can improve your application's performance.
⚡️ What dynamic rendering is and when to use it.
📦 Different approaches to make your dashboard dynamic.
🕒 Simulate a slow data fetch to see what happens.
🔗 How to implement streaming with "loading.tsx" and Suspense.
🩻 What loading skeletons are.
⏚ What route groups are, and when you might use them.

**What is Static Rendering?**

With static rendering, data fetching and rendering happens on the server at build time (when you deploy) or during [revalidation](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#revalidating-data). The result can then be distributed and cached in a [Content Delivery Network (CDN)](https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default).


![Diagram showing how users hit the CDN instead of the server when requesting a page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x1h0m1coexsdn589a7e7.png)

Whenever a user visits your application, the cached result is served. There are a couple of benefits of static rendering:

- **Faster Websites** - Prerendered content can be cached and globally distributed. This ensures that users around the world can access your website's content more quickly and reliably.
- **Reduced Server Load** - Because the content is cached, your server does not have to dynamically generate content for each user request.
- **SEO** - Prerendered content is easier for search engine crawlers to index, as the content is already available when the page loads. This can lead to improved search engine rankings.

Static rendering is useful for UI with no data or data that is shared across users, such as a static blog post or a product page. It might not be a good fit for a dashboard that has personalized data that is regularly updated.

The opposite of static rendering is dynamic rendering.

**What is Dynamic Rendering?**

With dynamic rendering, content is rendered on the server for each user at request time (when the user visits the page). There are a couple of benefits of dynamic rendering:

- **Real-Time Data** - Dynamic rendering allows your application to display real-time or frequently updated data. This is ideal for applications where data changes often.
- **User-Specific Content** - It's easier to serve personalized content, such as dashboards or user profiles, and update the data based on user interaction.
- **Request Time Information** - Dynamic rendering allows you to access information that can only be known at request time, such as cookies or the URL search parameters.

**Making the dashboard dynamic**

By default, `@vercel/postgres` doesn't set its own caching semantics. This allows the framework to set its own static and dynamic behavior.

You can use a Next.js API called `unstable_noStore` inside your Server Components or data fetching functions to opt out of static rendering. Let's add this.

In your `data.ts`, import `unstable_noStore` from `next/cache`, and call it the top of your data fetching functions:

```js
// ...
import { unstable_noStore as noStore } from 'next/cache';
 
export async function fetchRevenue() {
  // Add noStore() here to prevent the response from being cached.
  // This is equivalent to in fetch(..., {cache: 'no-store'}).
  noStore();
 
  // ...
}
 
export async function fetchLatestInvoices() {
  noStore();
  // ...
}
 
export async function fetchCardData() {
  noStore();
  // ...
}
 
export async function fetchFilteredInvoices(
  query: string,
  currentPage: number,
) {
  noStore();
  // ...
}
 
export async function fetchInvoicesPages(query: string) {
  noStore();
  // ...
}
 
export async function fetchFilteredCustomers(query: string) {
  noStore();
  // ...
}
 
export async function fetchInvoiceById(query: string) {
  noStore();
  // ...
}
```

**Simulating a Slow Data Fetch**

Making the dashboard dynamic is a good first step. However... there is still one problem. What happens if one data request is slower than all the others?

Let's simulate a slow data fetch. In our `data.ts` file, uncomment the `console.log` and `setTimeout` inside `fetchRevenue()`:

```js
export async function fetchRevenue() {
  try {
    // We artificially delay a response for demo purposes.
    // Don't do this in production :)
    console.log('Fetching revenue data...');
    await new Promise((resolve) => setTimeout(resolve, 3000));
 
    const data = await sql<Revenue>`SELECT * FROM revenue`;
 
    console.log('Data fetch completed after 3 seconds.');
 
    return data.rows;
  } catch (error) {
    console.error('Database Error:', error);
    throw new Error('Failed to fetch revenue data.');
  }
}
```
Now open `http://localhost:3000/dashboard/` in a new tab and notice how the page takes longer to load. In your terminal, you should also see the following messages:


```bash
Fetching revenue data...
Data fetch completed after 3 seconds.
```

Here, you've added an artificial 3-second delay to simulate a slow data fetch. The result is that now your whole page is blocked while the data is being fetched.

Which brings us to a common challenge developers have to solve:

With dynamic rendering, your application is only as fast as your slowest data fetch.

**What is streaming?**

Streaming is a data transfer technique that allows you to break down a route into smaller "chunks" and progressively stream them from the server to the client as they become ready.


![Diagram showing time with sequential data fetching and parallel data fetching](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5uwafmu818ekhmc78asw.png)

By streaming, you can prevent slow data requests from blocking your whole page. This allows the user to see and interact with parts of the page without waiting for all the data to load before any UI can be shown to the user.


![Diagram showing time with sequential data fetching and parallel data fetching](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72eysernwgaamjpko77b.png)

Streaming works well with React's component model, as each component can be considered a chunk.

There are two ways you implement streaming in Next.js:

- At the page level, with the `loading.tsx` file.
- For specific components, with `<Suspense>`.

Let's see how this works.


**Streaming a whole page with `loading.tsx`**

In the `/app/dashboard` folder, create a new file called `loading.tsx`:

```js
export default function Loading() {
  return <div>Loading...</div>;
}
```

Refresh `http://localhost:3000/dashboard`, and you should now see:


![Dashboard page with 'Loading...' text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/96zlx8w1yxz3an6xhzrn.png)


A few things are happening here:

1. `loading.tsx` is a special Next.js file built on top of Suspense, it allows you to create fallback UI to show as a replacement while page content loads.
2. Since `<SideNav>` is static, it's shown immediately. The user can interact with `<SideNav>` while the dynamic content is loading.
3. The user doesn't have to wait for the page to finish loading before navigating away (this is called interruptable navigation).

Congratulations! You've just implemented streaming. But we can do more to improve the user experience. Let's show a loading skeleton instead of the `Loading…` text.

**Adding loading skeletons**

A loading skeleton is a simplified version of the UI. Many websites use them as a placeholder (or fallback) to indicate to users that the content is loading. Any UI you embed into `loading.tsx` will be embedded as part of the static file, and sent first. Then, the rest of the dynamic content will be streamed from the server to the client.

Inside your `loading.tsx` file, import a new component called `<DashboardSkeleton>`:

```js
import DashboardSkeleton from '@/app/ui/skeletons';
 
export default function Loading() {
  return <DashboardSkeleton />;
}
```

Then, refresh http://localhost:3000/dashboard, and you should now see:


![Dashboard page with loading skeletons](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j3v6u1u8lmkfov0128ne.png)

**Fixing the loading skeleton bug with route groups**

Right now, our loading skeleton will apply to the invoices and customers pages as well.

Since `loading.tsx` is a level higher than `/invoices/page.tsx and /customers/page.tsx` in the file system, it's also applied to those pages.

We can change this with Route Groups. Create a new folder called `/(overview) `inside the dashboard folder. Then, move your `loading.tsx` and `page.tsx` files inside the folder:


![Folder structure showing how to create a route group using parentheses](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ckl5lb1qbhfc22nolr1v.png)

Now, the `loading.tsx` file will only apply to your dashboard overview page.

Route groups allow you to organize files into logical groups without affecting the URL path structure. When you create a new folder using parentheses `()`, the name won't be included in the URL path. So `/dashboard/(overview)/page.tsx` becomes `/dashboard`.

Here, you're using a route group to ensure `loading.tsx `only applies to your dashboard overview page. However, you can also use route groups to separate your application into sections (e.g. `(marketing)` routes and `(shop) `routes) or by teams for larger applications.

**Streaming a component**

So far, you're streaming a whole page. But, instead, you can be more granular and stream specific components using React Suspense. 

Check out the video for full explanation: {% youtube 2uey_tq1F8Y%}

Support me: Like, Share and Subscribe!