DEV Community

Cover image for Mastering Next.js 14: Static and Dynamic Rendering with Streaming
Coding Jitsu
Coding Jitsu

Posted on • Updated on

Mastering Next.js 14: Static and Dynamic Rendering with Streaming

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. The result can then be distributed and cached in a Content Delivery Network (CDN).

Diagram showing how users hit the CDN instead of the server when requesting a page

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:

// ...
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();
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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():

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.');
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

Fetching revenue data...
Data fetch completed after 3 seconds.
Enter fullscreen mode Exit fullscreen mode

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

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

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:

export default function Loading() {
  return <div>Loading...</div>;
}
Enter fullscreen mode Exit fullscreen mode

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

Dashboard page with 'Loading...' text

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>:

import DashboardSkeleton from '@/app/ui/skeletons';

export default function Loading() {
  return <DashboardSkeleton />;
}
Enter fullscreen mode Exit fullscreen mode

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

Dashboard page with loading skeletons

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

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.tsxonly 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:

Support me: Like, Share and Subscribe!

Top comments (0)