---
title: "\U0001F9EA Suspense | Experimental"
keywords: 'suspense, reveal, fallback, async rendering, loading, out-of-order streaming'
---

import CodeSandbox from '../../../../components/code-sandbox/index.tsx';
import { Note } from '~/components/note/note';

# Suspense

**Stage:** `implementation`

<Note>
  `Suspense` and out-of-order streaming are experimental. Their API and rendering behavior may still
  change as the feature develops.
</Note>

`<Suspense>` shows a fallback while its content is loading.

When the content is ready, Qwik hides the fallback and shows the real content.

Suspense does not fetch data or create async values. It only decides what to show while rendering is paused.

The high-level model is:

```tsx
useSignal()     // state you set directly
useComputed$()  // state calculated synchronously or from async work

<Suspense>      // fallback UI while content loads
```

The loading work happens elsewhere. Suspense only shows the fallback.

## Basic Usage

Wrap loading content with `<Suspense>` and pass a `fallback`:

<CodeSandbox src="/src/routes/demo/component/suspense/index.tsx" style={{ height: '14em' }}>
```tsx /Suspense/ /fallback/ /delay/
import { component$, Suspense, useSignal, type JSXOutput } from '@qwik.dev/core';

const AsyncMessage = component$(() => {
  const content = new Promise<JSXOutput>((resolve) => {
    setTimeout(() => resolve(<p>Async content resolved.</p>), 1000);
  });

  return <>{content}</>;
});

export default component$(() => {
  const show = useSignal(false);

  return (
    <section>
      <button onClick$={() => (show.value = !show.value)}>
        {show.value ? 'Hide' : 'Show'} content
      </button>

      {show.value && (
        <Suspense fallback={<p>Loading content...</p>} delay={150}>
          <AsyncMessage />
        </Suspense>
      )}
    </section>
  );
});
```
</CodeSandbox>

While `AsyncMessage` is loading, Qwik shows the fallback. Once it resolves, Qwik shows the real content.

## With `useComputed$()`

The computed signal loads the data. Suspense shows the fallback while `user.value` is not ready.

<CodeSandbox src="/src/routes/demo/component/suspense-async/index.tsx" style={{ height: '16em' }}>
```tsx /useComputed$/ /Suspense/ /user.value/
import { component$, Suspense, useComputed$, useSignal } from '@qwik.dev/core';

type User = {
  name: {
    first: string;
    last: string;
  };
  email: string;
};

const UserCard = component$(() => {
  const user = useComputed$(async ({ abortSignal }) => {
    const response = await fetch('https://randomuser.me/api/', {
      signal: abortSignal,
    });
    const data = (await response.json()) as {
      results: User[];
    };

    return data.results[0];
  });

  return (
    <p>
      User: {user.value.name.first} {user.value.name.last} ({user.value.email})
    </p>
  );
});

export default component$(() => {
  const show = useSignal(false);

  return (
    <section>
      <button onClick$={() => (show.value = !show.value)}>
        {show.value ? 'Hide user' : 'Load random user'}
      </button>

      {show.value && (
        <Suspense fallback={<p>Loading user...</p>}>
          <UserCard />
        </Suspense>
      )}
    </section>
  );
});
```
</CodeSandbox>

When `UserCard` reads `user.value` before the request finishes, Qwik shows the nearest `<Suspense>` fallback.

Inline `.pending` and `.error` checks are still useful when loading, error, and success states need different markup. Suspense works well when one fallback can cover the whole section.

## What Can Trigger Suspense

Suspense can show its fallback when content inside it is still loading, including:

- reading an async `useComputed$()` value before it is ready
- returning a `Promise` from JSX
- rendering a child component that is still loading
- running descendant work that blocks rendering

Suspense does not do the loading work. It only controls the fallback while that work finishes.

## Out-of-order Streaming

Out-of-order streaming is an optional server rendering mode for `<Suspense>`.

Server rendering means Qwik creates HTML on the server and sends it to the browser. Streaming means
the browser can receive that HTML in chunks, within the same HTTP connection, instead of waiting for
one complete HTML string.

Out-of-order streaming helps when one section of the page is slow, but the rest of the page is ready.

Without out-of-order streaming, the server renders the page in order. If a Suspense child is still
waiting, the server waits at that point before it can continue sending the next HTML.

With out-of-order streaming, Qwik can:

1. Send the HTML that is ready.
2. Send the Suspense fallback for that section.
3. Continue sending the rest of the page, such as the footer or nearby buttons.
4. Send the real Suspense content later, when it finishes.
5. Hide the fallback and show the real content in the browser.

During SSR, Qwik includes the fallback in the stream when the boundary has a fallback. Qwik hides
that fallback when the resolved content is shown. It stays in the DOM; it is not removed.

If the boundary has a positive `delay`, Qwik still sends the fallback HTML immediately, but starts
it hidden. If the delay finishes before the content, Qwik reveals the fallback. If the content
finishes before the delay, the fallback remains hidden and the resolved content is shown instead.

From the user's point of view, the page appears sooner. They can see the shell of the page while
one slower section is still loading.

<Note>
Out-of-order streaming only applies to promises inside `<Suspense>`. A promise outside Suspense still
follows the normal SSR rendering behavior.
</Note>

You do not need to write the streaming markers or reveal scripts yourself. Keep writing
`<Suspense>` and fallbacks normally; Qwik handles the streamed HTML and browser-side reveal.

### Enable Suspense

Suspense is experimental, so first enable it in `vite.config.ts`:

```ts title="vite.config.ts" /experimental/
import { qwikVite } from '@qwik.dev/core/optimizer';
import { qwikRouter } from '@qwik.dev/router/vite';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig(() => ({
  plugins: [
    qwikRouter(),
    qwikVite({
      experimental: ['suspense'],
    }),
    tsconfigPaths(),
  ],
}));
```

### Configure Out-of-order Streaming

After Suspense is enabled, out-of-order streaming is enabled by default. You can also set it
explicitly in your server entry point.

Most Qwik apps have a `src/entry.ssr.tsx` file. That file receives render options from the adapter
and returns the JSX and options that should be rendered.

Add `streaming.outOfOrder` to those options if you want to set it directly:

```tsx title="src/entry.ssr.tsx" /outOfOrder/
import { createRenderer } from '@qwik.dev/router';
import Root from './root';

export default createRenderer((opts) => {
  return {
    jsx: <Root />,
    options: {
      ...opts,
      streaming: {
        ...opts.streaming,
        outOfOrder: true,
      },
    },
  };
});
```

The enabled value is:

```ts
outOfOrder: true
```

To turn it off again, use:

```ts
outOfOrder: false
```

If your project calls `renderToStream()` directly, pass the same `streaming` option when you want to
set it explicitly:

```tsx title="src/entry.ssr.tsx" /outOfOrder/
import { renderToStream, type RenderToStreamOptions } from '@qwik.dev/core/server';
import Root from './root';

export default function (opts: RenderToStreamOptions) {
  return renderToStream(<Root />, {
    ...opts,
    streaming: {
      ...opts.streaming,
      outOfOrder: true,
    },
  });
}
```

### Example

In this example, the page shell and fallback can stream before `ProfileDetails` finishes:

```tsx /Suspense/ /fallback/ /profile.value/
import { component$, Suspense, useComputed$ } from '@qwik.dev/core';

const ProfileDetails = component$(() => {
  const profile = useComputed$(async ({ abortSignal }) => {
    const response = await fetch('/api/profile', {
      signal: abortSignal,
    });

    return response.json() as Promise<{
      name: string;
      plan: string;
    }>;
  });

  return (
    <section>
      <h2>{profile.value.name}</h2>
      <p>Plan: {profile.value.plan}</p>
    </section>
  );
});

export default component$(() => {
  return (
    <main>
      <h1>Account</h1>

      <Suspense fallback={<p>Loading profile...</p>}>
        <ProfileDetails />
      </Suspense>

      <footer>Need help? Contact support.</footer>
    </main>
  );
});
```

If `/api/profile` is slow, Qwik can send:

- the `Account` heading
- the `Loading profile...` fallback
- the footer

Then, when the profile request resolves, Qwik sends the finished profile HTML and shows it in the
Suspense boundary.

If the profile request finishes quickly, Qwik still sends the fallback during SSR, but the finished
profile can be shown before the user notices it.

### Fallbacks

The fallback is real UI. Choose markup that is useful while the user waits:

```tsx
<Suspense fallback={<p>Loading recommendations...</p>}>
  <Recommendations />
</Suspense>
```

If you do not provide a fallback, the boundary has no visible loading UI. The rest of the page can
still stream, but that section will appear empty until the content is ready.

When out-of-order streaming is enabled, a boundary that suspends during the initial server render
streams its fallback as soon as Qwik reaches that boundary. Without a positive `delay`, the fallback
can be visible immediately. With a positive `delay`, the fallback is streamed hidden and is only
revealed if the delay finishes before the real content.

For best results:

- keep fallbacks small
- reserve enough space to avoid layout shift
- use accessible text, not only a spinner
- avoid important actions that disappear immediately when the real content arrives

### Interactivity

Qwik keeps the streamed HTML resumable.

That means the shell of the page can become interactive even while a Suspense section is still
waiting. If your fallback has buttons or other event handlers, they can also resume while the
fallback is visible, after the root page state has loaded in the browser.

When the real content arrives, Qwik hides the fallback and shows the resolved content. The fallback
stays in the DOM. The resolved content may be visible before it is interactive. Qwik resumes that
content after the root page state and the content's streamed state have both loaded, then its event
handlers and state work like any other server-rendered Qwik HTML.

### When to Use It

Out-of-order streaming is useful for independent page sections that may be slower than the rest of
the page, such as:

- account panels
- recommendations
- reviews
- dashboards
- comments
- secondary content below the main heading

It is less useful when the slow content is required before the rest of the page makes sense. In that
case, showing a complete page a little later may be clearer than streaming a partial page early.

### Static Builds

Out-of-order streaming is an SSR feature. It helps when the browser is receiving an HTTP response
while the server is still rendering.

For static site generation, the HTML file is created ahead of time. The user receives the finished
file later, so there is no live server stream to reveal a fallback first.

For this reason, Qwik Router disables out-of-order streaming during SSG. Static pages are written as
complete HTML files even if the server entry point sets `streaming.outOfOrder` to `true`.

### Troubleshooting

If you see an error that says Suspense must be enabled, add `experimental: ['suspense']` to
`qwikVite()`.

If the fallback is not visible, check these common causes:

- the resolved content is shown before the browser paints the fallback
- the slow promise is outside `<Suspense>`
- there is no `fallback` prop
- the hosting platform or proxy buffers the whole response instead of streaming it

If your site uses a Content Security Policy with script nonces, keep passing the nonce through your
SSR render options. Qwik will add the nonce to the inline scripts it uses for out-of-order
streaming.

## `delay`

The `delay` prop waits before showing the fallback:

<CodeSandbox src="/src/routes/demo/component/suspense-delay/index.tsx" style={{ height: '17em' }}>
```tsx /delay/
import {
  component$,
  Suspense,
  useSignal,
  type JSXOutput,
} from '@qwik.dev/core';

const LOAD_MS = 2500;
const FALLBACK_DELAY_MS = 1000;

const SlowContent = component$(() => (
  <>
    {new Promise<JSXOutput>((resolve) => {
      setTimeout(() => resolve(<p>Loaded content.</p>), LOAD_MS);
    })}
  </>
));

export default component$(() => {
  const run = useSignal(0);
  const elapsed = useSignal(0);

  return (
    <section>
      <button
        disabled={run.value > 0 && elapsed.value < LOAD_MS}
        onClick$={() => {
          run.value++;
          elapsed.value = 0;

          const start = Date.now();
          const timer = setInterval(() => {
            elapsed.value = Math.min(Date.now() - start, LOAD_MS);

            if (elapsed.value === LOAD_MS) {
              clearInterval(timer);
            }
          }, 100);
        }}
      >
        Load content
      </button>

      {run.value > 0 && (
        <Suspense
          fallback={<p>Fallback shown after {FALLBACK_DELAY_MS}ms.</p>}
          delay={FALLBACK_DELAY_MS}
        >
          <SlowContent key={run.value} />
        </Suspense>
      )}

      <p>Elapsed: {elapsed.value}ms</p>
    </section>
  );
});
```
</CodeSandbox>

Use `delay` to avoid flashing a loading state for work that usually resolves quickly.

If the content resolves before the delay finishes, the fallback is not shown.

During out-of-order SSR streaming, Qwik still sends the fallback HTML immediately so the browser can
reveal it without waiting for another HTML chunk. For `delay > 0`, the fallback starts hidden. If
the delay finishes first, Qwik reveals it with a streamed update. If the content finishes first, Qwik
cancels the reveal and shows the resolved content instead.

After a boundary shows its content, later asynchronous updates keep that content visible and do not
show the fallback again. A newly rendered boundary can show its fallback during its initial load.

## Coordinating Boundaries with `Reveal`

Use `<Reveal>` when a group of sibling `<Suspense>` boundaries should reveal in a specific order.

```tsx /Reveal/ /order/ /collapsed/
import { component$, Reveal, Suspense } from '@qwik.dev/core';

export default component$(() => {
  return (
    <Reveal order="sequential" collapsed>
      <Suspense fallback={<p>Loading profile...</p>}>
        <Profile />
      </Suspense>

      <Suspense fallback={<p>Loading activity...</p>}>
        <Activity />
      </Suspense>
    </Reveal>
  );
});
```

`Reveal` does not start or resolve async work. It only coordinates which registered boundary is allowed to show its content.

Reveal order options:

- `parallel`: boundaries reveal independently
- `sequential`: later boundaries wait for earlier pending boundaries
- `reverse`: earlier boundaries wait for later pending boundaries
- `together`: no boundary reveals content until all boundaries are ready

By default, a pending boundary that is blocked by `Reveal` can still show its fallback. Add `collapsed` to hide blocked pending boundaries completely until they are allowed to reveal.

## API

```tsx
type SuspenseProps = {
  fallback?: JSXOutput;
  delay?: number;
};

type RevealOrder = 'parallel' | 'sequential' | 'reverse' | 'together';

type RevealProps = {
  order?: RevealOrder;
  collapsed?: boolean;
};
```

Props:

- `fallback`: JSX rendered while the boundary is waiting
- `delay`: milliseconds to wait before showing the fallback, defaults to `0`
- `order`: reveal order for child Suspense boundaries, defaults to `parallel`
- `collapsed`: hide pending blocked boundaries instead of showing their fallbacks

## Suspense Is Not an Error Boundary

`<Suspense>` only handles content that is still loading. It does not catch errors thrown by child components.

For errors thrown by descendants, reach for [`useErrorBoundary()`](/api/qwik/#useerrorboundary).

## Choosing the Right Primitive

| API | Best for |
| --- | --- |
| `useComputed$()` | Derived values, including async data or other async work that should produce a signal |
| `<Suspense>` | Showing one fallback while a section is loading |
| `useTask$()` | Running setup or update code that writes to existing state or performs a side effect |
| `routeLoader$()` | Loading route data before the route renders |

For async work inside a component, `useComputed$()` with an async function is usually the starting point. Add `<Suspense>` when one fallback can cover the loading state for a section. Keep inline `.pending` and `.error` checks when each state needs different markup.
