Tutorial 8 of 36 · for anyone with one loader that is slower than the rest of the page · Markdown

Stream a slow page

You'll take a page that waits four seconds on a recommendation service and make the rest of it arrive immediately, with the slow region filling in where it sits. The loading state is one small component in its own file.

Before you start

Install from crates.io. You need no Node and no package manager.

cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version

Every command and screenshot on this page was captured with fsr 0.x.

Start with the slow page#

app/routes/product/[id]/page.loader.ts
import type { Ctx } from "@snapfire/fsr";

export async function load({ params, services }: Ctx<"/product/{id}"> ) {
  const product = await services.shopping.getProduct({ id: params.id });
  const suggestions = await services.recommend.forProduct({ id: params.id });
  return { product, suggestions };
}

The recommendation call takes four seconds. The product call takes twelve milliseconds. Nothing renders until both are done, so every visitor waits four seconds for a page that was ready almost immediately.

Split the slow half into its own segment#

The slow call only feeds one region of the page, so move it into a parallel slot. app/routes/product/[id]/ gets a sibling directory under the layout that places it:

app/routes/
  layout.tsx      layout.loader.ts
  slots/suggestions/
    page.tsx
    page.loader.ts
    loading.tsx
  product/[id]/
    page.tsx
    page.loader.ts

slots/suggestions/page.loader.ts holds the slow call and nothing else:

app/routes/slots/suggestions/page.loader.ts
import type { Ctx } from "@snapfire/fsr";

export async function load({ services, params }: Ctx) {
  const suggestions = await services.recommend.forProduct({ id: params.id });
  return { suggestions };
}

The page's own loader keeps the fast call:

app/routes/product/[id]/page.loader.ts
import type { Ctx } from "@snapfire/fsr";

export async function load({ params, services }: Ctx<"/product/{id}">) {
  const product = await services.shopping.getProduct({ id: params.id });
  return { product };
}

Add a loading file#

app/routes/slots/suggestions/loading.tsx
export default function Loading() {
  return <div className="suggestions-skeleton" aria-busy="true" />;
}

That is loading.tsx beside the slot's page. A segment with a loading.tsx is marked deferred in the plan. The document ships with the loading module in that region and the real one streams in when its loader finishes, filling the region in place.

Adding the file turns streaming on. The page and the loader stay as they are.

Place the slot in the layout#

The layout places the region:

app/routes/layout.tsx
import { Slot } from "@snapfire/fsr-client/react";

export default function Layout({ children }: { children: unknown }) {
  return (
    <div className="page">
      {children}
      <Slot name="suggestions" />
    </div>
  );
}

Watch it arrive#

curl -N localhost:11110/product/3

-N turns off curl's buffering so you see the response as the host writes it. The document arrives at once with the skeleton in place, then four seconds later the rest of the body lands carrying the region's markup. It is one response written in two parts.

In a browser the region fills in without the page moving, as long as your skeleton is the size of what replaces it. Size the skeleton in CSS, because a region that grows when it fills pushes everything below it down the page.

Prerendering and status codes#

A deferred segment cannot be prerendered as one document, because the document is finished before the segment is. Chapter 110 covers pages that don't change between deploys. Don't use prerendering and streaming on the same route.

The status is decided by the page, not by the slot. If the recommendation service is down, the region renders its error boundary and the document still answers 200, because the product itself still loaded. The rest of the page is still usable, so a 200 is usually correct. An alert built on status codes won't see a broken recommendation service, since it never answers 500.

Which segments to defer#

Deferring is worth it when one segment is genuinely slower than the rest. Defer the fast half too and you have shipped an empty document with four skeletons that all fill in at once, which is slower to read and looks broken. Measure which loader is slow first: 420 reads the spans that tell you.

Next up: 150. Share state with the store.