---
title: "Make a page fast"
audience: "anyone with a slow page"
description: "420 reads a trace to find out what a request did. This tutorial uses the trace to find which part of a slow page to cut and to check that the cut worked. The…"
snippet_caption: "terminal"
snippet: "curl -s localhost:11110/__fsr/traces | head -40"
---

import { Install } from "@src/ui/Install";

[420](https://snapfirers.com/fsr/learn/420-debug-a-page-in-production) reads a trace to find out what a request did. This tutorial uses the trace to find which part of a slow page to cut and to check that the cut worked. The changes below are ordered by how much they usually save.

<Install captured="0.x" />

## Find the slow part

```sh
curl -s localhost:11110/__fsr/traces | head -40
```

A trace has a span per loader, per service call and per render node. Read the `source` spans first: they are your loaders, one row each, with the time they took. A page that takes 900ms with one loader at 870ms has a slow loader and nothing else to fix.

Three caches report themselves on those spans. A `source` span carries `memo: hit` or `memo: miss` when that loader is memoizable. A `call` span carries `cache: hit` or `cache: miss` when the method has a cache policy. A `render` span carries the same for the render cache. A `cache: hit` high in the render tree means the nodes beneath it were never rendered at all, so they have no spans. When a subtree gets *shorter* between two requests, the cache is working and the trace hasn't been truncated.

> Don't guess from the browser. A network panel shows that the document took 900ms but can't show which of the four things the document waited on.

## Memoize or prerender the page

A route whose loader reads nothing request-specific is a candidate for the render memo; one that reads nothing at all is a candidate for prerendering. [100](https://snapfirers.com/fsr/learn/100-cache-loaders-and-renders) covers this in full. The host works it out from what the loader reads, so to make a page memoizable, stop reading the request in it.

Often a single line causes it. A layout loader that reads `session` for a cart count makes every page under it per session, including the pages that have nothing to do with a cart. Move the cart count into a parallel slot with its own loader and the rest of the layout goes back to being one render for everybody.

This change usually saves the most. The fix is a file move, so it doesn't look like a performance change.

## Serial awaits

Two awaits in a row are two round trips added together:

```ts
const product = await services.shopping.getProduct({ id });
const reviews = await services.reviews.forProduct({ id });
```

Neither needs the other, so the page waits for the sum of the two calls instead of the longer one.

If one of them is genuinely slower than the page, give it its own segment and let it stream: [140](https://snapfirers.com/fsr/learn/140-stream-a-slow-page) shows how. That takes the slow call off the critical path instead of shortening it.

## Cache repeated service calls

A service method with a cache policy answers the second identical call from memory; the `call` span says `cache: hit` when it did. You set this in configuration. Check the spans before reaching for it: a call at 6ms doesn't need a cache and caching it only adds an invalidation problem.

## Measure the change

Measure before and after with the same build, the same data and the same request. Caches change the numbers, so request the page twice in each run and compare the second traces.

Compare span times, not how fast the page feels. For example, "the source span went from 870ms to 40ms".

Next up: [440. Find broken links](https://snapfirers.com/fsr/learn/440-find-broken-links).
