Skip to content

Remix Deferred Loading — Streaming and Progressive Rendering

DodaTech Updated 2026-06-28 3 min read

In this tutorial, you will learn about Remix Deferred Loading. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn Remix deferred loading: stream critical data first, defer non-critical data, and progressively render content with Suspense for faster initial page loads.

In this lesson, you'll use defer() to send critical data immediately and stream non-critical data, rendering it as it arrives.

What You'll Learn

How to use defer() for streaming responses, render deferred data with <Await>, prioritize critical content, and handle loading states.

Why It Matters

Deferred loading improves perceived performance by rendering the page with essential data first and streaming less important data as it becomes available.

Real-World Use

DodaZIP's dashboard loads user profile and metrics immediately while streaming audit logs and recommendations that are less time-sensitive.

flowchart LR
    A[Loader] --> B[defer()]
    B --> C[Critical: Instant]
    B --> D[Deferred: Stream]
    C --> E[Render Immediately]
    D --> F[Render When Ready]
    style B fill:#121212,color:#fff

Using defer()

import { defer } from "@remix-run/node";
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";

export const loader = async () => {
  // Critical data: fetched immediately
  const user = await getUser();

  // Non-critical data: deferred
  const recommendations = getRecommendations(user.id); // Promise

  return defer({
    user,
    recommendations, // This is a promise
  });
};

export default function Dashboard() {
  const { user, recommendations } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <p>This renders immediately.</p>

      <Suspense fallback={<p>Loading recommendations...</p>}>
        <Await resolve={recommendations}>
          {(recs) => (
            <ul>
              {recs.map(rec => <li key={rec.id}>{rec.title}</li>)}
            </ul>
          )}
        </Await>
      </Suspense>
    </div>
  );
}

Multiple Deferred Values

export const loader = async () => {
  return defer({
    user: getUser(),
    notifications: getNotifications(),
    suggestions: getSuggestions(),
    metrics: getMetrics(), // This one might be slow
  });
};

export default function App() {
  const data = useLoaderData<typeof loader>();

  return (
    <div>
      <Suspense fallback="Loading user...">
        <Await resolve={data.user}>{(user) => <UserProfile user={user} />}</Await>
      </Suspense>

      <Suspense fallback="Loading notifications...">
        <Await resolve={data.notifications}>{(n) => <NotificationBadge count={n.length} />}</Await>
      </Suspense>

      <Suspense fallback="Loading suggestions...">
        <Await resolve={data.suggestions}>{(s) => <SuggestionsList items={s} />}</Await>
      </Suspense>
    </div>
  );
}

Error Handling with Await

Handle errors from deferred promises:

<Suspense fallback={<p>Loading recommendations...</p>}>
  <Await
    resolve={recommendations}
    errorElement={<p>Failed to load recommendations</p>}
  >
    {(recs) => <RecommendationsList items={recs} />}
  </Await>
</Suspense>

Streaming with HTTP/2

Deferred loading uses Remix's streaming response. The initial chunk contains the critical HTML. Subsequent chunks stream as deferred promises resolve.

Common Mistakes

  1. Deferring critical data: Only defer non-essential content. Data the user sees above the fold should be instant.
  2. Not wrapping in <Suspense>: <Await> must be wrapped in <Suspense>. Without it, the component throws during rendering.
  3. Forgetting error handling: Deferred promises can reject. Always provide an errorElement or error boundary.
  4. Over-deferring: Each deferred value adds streaming overhead. Defer only 2-3 large, non-critical data sources.
  5. Not testing on slow connections: Verify the streaming experience under throttled network conditions.

Practice Questions

  1. What does defer() return? Answer: A response that streams critical data immediately and defers promises for non-critical data.

  2. How do you render deferred data in a component? Answer: Use <Await resolve={promise}> wrapped in <Suspense fallback={...}>.

  3. What happens if a deferred promise rejects? Answer: The <Await> component renders the errorElement or throws to the nearest error boundary.

  4. When should you NOT use deferred loading? Answer: For critical above-the-fold content or when the deferred data is needed for the initial render.

Challenge

Build a profile page that loads user info immediately, defers the friends list (simulate 2s delay), and defers the activity feed (simulate 4s delay). Each section should show its own loading state.

Mini Project

Create a project dashboard with three sections: critical metrics (instant), project list (deferred, 1s), and team activity feed (deferred, 3s). Each section streams in as it resolves.

FAQ

Does deferred loading work with static generation?

: No. Deferred loading requires SSR with streaming support.

Can I use deferred loading with resource routes?

: Yes. Resource routes can use defer() for streaming JSON responses.

What happens if the client disconnects during streaming?

: Remix handles disconnection gracefully. The stream is aborted on the server.

Does deferred loading work with all hosting platforms?

: Streaming requires platforms that support HTTP streaming. Vercel, Fly.io, and Node.js support it. Some Serverless platforms may not.

What's Next

Learn about Remix Resource Prefetching for speculative loading of resources before navigation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro