WDD 430: Web Full-Stack Development

Streaming

Overview

Slow data fetches can impact the performance of web applications. Streaming is a technique for delivering content to users as it becomes available, rather than waiting for the entire resource to be generated or downloaded. This approach can improve perceived performance and user experience, especially for large files or real-time data.

Prepare

With streaming data, you can start processing and displaying content as soon as it begins to arrive, rather than waiting for the entire resource to be generated or downloaded. This can significantly improve the perceived performance of your application, especially when dealing with large files or real-time data. In this activity, you will prepare your application to handle streaming data effectively.

Streaming works well with React's Suspense and Server Components. Each component can be considered to be a chunk of the overall UI.

Diagram of streaming UI segments: page shell first, then fetching, rendering, loading fallback, and hydrating in sequence
Timeline showing three portfolio components — HeroSection, ProjectList, and AboutSection — each independently progressing through phases 1 (fetching), 2 (rendering), 3 (loading), and 4 (hydrating) at different speeds along a shared time axis
Streaming Components Independently

Implementing Streaming

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

— Streaming a Whole Page

To stream an entire page, you can create a loading.tsx file in the same directory as your page component. This file will define a React component that serves as a fallback while the page is loading. When the page is requested, Next.js will render the loading component immediately and then stream the actual page content as it becomes available.

For example, if your portfolio home page is in app/page.tsx, you can create an app/loading.tsx file with the following content:

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

Note the following:

Loading Skeletons vs Text

Instead of displaying a simple loading message, you can create a more engaging user experience by using loading skeletons. These are placeholder elements that mimic the structure of the content being loaded, providing a simplified, visual cue to users about what to expect.

Leveraging AI

This is a good opportunity to use AI to help draft a skeleton file for your portfolio page. Ask AI to generate a simple app/loading.tsx that matches the major sections of your app/page.tsx, then review, simplify, and test the result so you understand every line you keep.

For example, you can use gray boxes or animated placeholders to indicate where images, text, or other components will appear once they are fully loaded.

In the loading.tsx file:

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

For your portfolio app, a good skeleton should roughly match the structure of the page so users can immediately tell what is loading. For example, your app/loading.tsx file might look like this:

Mock portfolio home page with a loaded hero section and a project card grid where one project card is rendered while other cards still show skeleton placeholders during streaming
export default function Loading() {
  return (
    <main className="animate-pulse space-y-10 p-6">
      <section className="space-y-4">
        <div className="h-10 w-2/3 rounded bg-slate-200" />
        <div className="h-4 w-full rounded bg-slate-200" />
        <div className="h-4 w-5/6 rounded bg-slate-200" />
        <div className="h-10 w-40 rounded bg-slate-300" />
      </section>

      <section className="space-y-4">
        <div className="h-8 w-48 rounded bg-slate-200" />
        <div className="grid gap-4 md:grid-cols-2">
          <div className="h-40 rounded-xl bg-slate-200" />
          <div className="h-40 rounded-xl bg-slate-200" />
        </div>
      </section>

      <section className="space-y-4">
        <div className="h-8 w-40 rounded bg-slate-200" />
        <div className="h-4 w-full rounded bg-slate-200" />
        <div className="h-4 w-4/5 rounded bg-slate-200" />
      </section>
    </main>
  );
}
Fixing the Skeleton Bug

In the Route Groups learning activity, you will see that the skeleton applies to all the pages since the loading.tsx file is a level higher than the projects/ pages and about/ pages, and so on., and the skeleton layout is applied to all these pages. This bug or unintended UI behavior can be changed by using Route Groups.

— Streaming Individual Components

For more granular control, you can use React's <Suspense> component to stream individual components within a page. This allows you to display different loading states for different parts of the UI, improving the user experience by providing feedback on which content is loading.

To stream a component, wrap it in a <Suspense> component and provide a skeleton fallback. For example, on your school projects page you might have an async SchoolProjectList component that fetches from a database. Wrapping it in <Suspense> lets the rest of the page render immediately while that component loads independently:

// app/(projects)/school/page.tsx
import { Suspense } from 'react';
import SchoolProjectList from '@/app/ui/school-project-list';
import ProjectCardSkeleton from '@/app/ui/skeletons';

export default function SchoolPage() {
  return (
    <main>
      <h1>School Projects</h1>

      {/* Skeleton renders immediately; list streams in when data is ready */}
      <Suspense fallback={<ProjectCardSkeleton />}>
        <SchoolProjectList />
      </Suspense>
    </main>
  );
}

SchoolProjectList is an async Server Component — it awaits a data fetch internally. Because it is wrapped in <Suspense>, Next.js streams its HTML chunk to the client as soon as it resolves, without blocking the rest of the page.

Where to place <Suspense> boundaries depends on a few things:

The overall goal with Streaming and Server Components is to improve the user experience by allowing parts of the UI to load independently and display loading states for each component.

Activity Instructions

Apply both streaming approaches to your portfolio app — one per route.

Full-Page Streaming on Open-Source Projects Page

  1. Create a loading.tsx file inside your open-source projects route folder (for example, app/(projects)/open-source/loading.tsx). Build a skeleton that roughly matches the layout of that page using the same Tailwind approach shown above.
  2. To make the effect visible in development, temporarily add an artificial delay to the data-fetching function used by that page:
    // Temporarily in your data fetch — remove after testing
    await new Promise(res => setTimeout(res, 2000));
  3. Navigate to the open-source projects page and confirm the skeleton renders immediately before the real content replaces it.
  4. Remove the delay when you are satisfied the skeleton is working correctly.

Component-Level Streaming on School Projects Page

  1. On your school projects page, move data fetching into a dedicated async Server Component (for example, SchoolProjectList) if it is not already structured that way.
  2. Wrap it in a <Suspense> boundary using a skeleton fallback, following the pattern shown in the example above.
  3. Add a temporary delay inside the component's data fetch function, then run the app and confirm the skeleton shows while data loads independently from the rest of the page.
  4. Remove the delay when done.

Route Group Scoping Check

Verify the loading.tsx from Part 1 only affects the open-source route. If the skeleton appears on unrelated pages, use a route group to scope it correctly. Refer to the Route Groups activity for guidance.