# Fresh Data at Scale

Course: [Astro](https://www.devtutorials.ai/learn/astro) · Section: Ship It
Source: https://www.devtutorials.ai/learn/astro/ship-it/fresh-data-at-scale

> Why one fresh part slows a whole page, server islands with server:defer, fallback content, encrypted props and cookies in server islands, Cache-Control and CDN caching, live collections with defineLiveCollection(), getLiveCollection() and getLiveEntry(), and error handling.

## Fresh Data at Scale

- Some parts of a page change every second: stock left, prices, "Hello, Asha".
- If one small part is personal or slow, the **whole page** must wait for it, and cannot be cached.
- Astro has two tools for this:
  - **Server islands** render that one part later, on the server, while the rest of the page is fast and cacheable.
  - **Live collections** load fresh data on every request, with the same style as content collections.
- In this lesson, you will also tell browsers and CDNs how long to keep a page with **cache headers**.

**Simple lesson:** keep most of the page fast and cached, and load only the fresh part on each request.

### A Real-World Example

- Asha opens a small shop page in her journal.
- The product text is the same for everyone. It can be built once.
- The "Only 3 left" number changes all the time.
- She wraps the stock box in a **server island**. The page loads at once with "Checking stock…", then the real number appears.

Press **▶ Run**. The page shows the fallback first. A moment later, the **Terminal** shows the island's own request, and the stock appears.

`,
    "src/pages/index.astro": `---
---

<h1>Astro sticker</h1>
<p>A shiny sticker for your laptop. ₹50.</p>
    <p slot="fallback">Checking stock…</p>
`,
  }}
/>

What happened:

- The page itself is **static**: it was built ahead of time (`▶ /` in the Terminal).
- `server:defer` left a placeholder with the **fallback** content.
- The browser then asked the server for the island alone (`/_server-islands/StockLeft`).
- The server rendered `StockLeft` fresh and the browser swapped it in.

### What You Will Learn

In this lesson, you will learn:

1. Why one fresh part can slow down a whole page.
2. How to make a server island with `server:defer`.
3. How to show a fallback while it loads.
4. How props and cookies work inside server islands.
5. How to cache pages with `Cache-Control` headers.
6. How to load fresh data with live collections.
7. How to handle live collection errors.

## Server Islands

### The Problem They Solve

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 220" role="img" aria-labelledby="fig-si" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-si">Animation: the static page arrives at once from the cache with a grey placeholder. A second, smaller request then fills the placeholder with fresh stock data.</title>
  <rect x="30" y="30" width="250" height="160" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} />
  <text x="155" y="54" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Page (cached, instant)</text>
  <rect x="50" y="68" width="210" height="40" rx="4" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)" }} />
  <text x="155" y="93" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 11 }}>Product text</text>
  <rect x="50" y="122" width="210" height="48" rx="6" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-orange)", strokeDasharray: "5 4" }} />
  <text x="155" y="150" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 11 }}>
    Checking stock…
    <animate attributeName="opacity" values="1;1;0;0" keyTimes="0;0.6;0.65;1" dur="4s" repeatCount="indefinite" />
  </text>
  <text x="155" y="150" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>
    Only 3 left!
    <animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.6;0.65;1" dur="4s" repeatCount="indefinite" />
  </text>
  <rect x="440" y="80" width="170" height="80" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} />
  <text x="525" y="112" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Server</text>
  <text x="525" y="132" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--mono)", fontSize: 10 }}>/_server-islands/StockLeft</text>
  <path d="M262 140 H436" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, strokeDasharray: "4 4" }} />
  <circle r="6" cy="140" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cx" values="262;436;262" keyTimes="0;0.3;0.6" dur="4s" repeatCount="indefinite" /></circle>
</svg>
<figcaption>The page comes first. The island follows with its own small request.</figcaption>
</figure>

- Without islands, a personal or slow part makes the whole page on-demand.
- On-demand pages are slower and harder to cache.
- With a server island, the page can stay static or cached. Only the island runs on every request.
- Server islands are **not** client islands. They run on the **server** and send **no JavaScript framework**. Only a tiny script fetches their HTML.

### Making a Server Island

1. Write a normal `.astro` component.
2. Add `server:defer` where you use it: `<StockLeft server:defer />`.
3. Add an adapter. Server islands are rendered on demand, so the site needs a server.

- The page around the island can still be prerendered (static).
- Many server islands on one page load **in parallel**.

### Showing a Fallback

- Put content in `slot="fallback"` to show it while the island loads.
- A good fallback has the same size as the result, so the page does not jump.
- Common fallbacks: "Loading…", a grey box, a generic avatar.

Server islands need an **adapter**, such as `@astrojs/node`, Netlify, Vercel or Cloudflare. Without one, Astro stops with an error.

**Simple lesson:** `server:defer` renders a component later, on the server, with a fallback until then.

## Props and Personal Data in Server Islands

### Passing Props

- You can pass props: `<StockLeft id="mug" server:defer />`.
- Astro **encrypts** the props and puts them in the island's URL, so visitors cannot read or change them.
- Props must be data: text, numbers, arrays, plain objects. No functions.
- Keep props small. Big props make the URL long (Astro switches to a POST request if it gets too long).

Press **▶ Run**. Three islands share one component, each with its own `id` prop. They load in parallel.

`,
    "src/pages/index.astro": `---
const ids = ["sticker", "mug", "tee"];
---

<h1>Journal shop</h1>
<ul>
    {ids.map((id) => (
        <li slot="fallback">Loading {id}…</li>
    ))}
</ul>
`,
  }}
/>

- **[JS]** Leave out `interface Props`.

### Cookies and Personal Content

- A server island gets the **visitor's request**, cookies included.
- So a static, cached page can still say "Hello, Asha" in its header.
- Inside the island, `Astro.url` is the island's own address. To know which page it is on, read `Astro.request.headers.get("Referer")`.

Press **▶ Run**. The home page says "Hello, guest". Click **Log in as Asha**. Back on the same static page, the island now greets Asha.

`,
    "src/pages/index.astro": `---
---

<header>
      <p slot="fallback">Hello…</p>
</header>
<h1>Asha's Dev Journal</h1>
<a href="/login">Log in as Asha</a>
`,
    "src/pages/login.ts": `import type { APIRoute } from "astro";

    cookies.set("name", "Asha", { path: "/" });
    return redirect("/");
};
`,
  }}
/>

- **[JS]** In `login.js`, leave out `import type` and `: APIRoute`.

Do not put personal data in a page that a CDN caches for everyone. Put it in a server island, so only the island is personal.

**Simple lesson:** server islands get props (encrypted) and the visitor's cookies, so cached pages can show personal parts.

## Caching Pages

### Cache-Control Headers

- A **cache** keeps a copy of a response, so the next visitor gets it without running your code.
- Browsers cache. **CDNs** (servers near your visitors) cache too.
- An on-demand page sets how long with the `Cache-Control` header:

<div className="tscroll">

| Header value | Meaning |
|---|---|
| `public, max-age=60` | Anyone may keep it for 60 seconds |
| `private, no-store` | Never cache (personal pages) |
| `public, s-maxage=600` | CDNs keep it 10 minutes, browsers follow `max-age` |
| `stale-while-revalidate=30` | Serve the old copy while fetching a new one |

</div>

- Some hosts also read `CDN-Cache-Control`, a header only for the CDN.

Press **▶ Run**. Open **/prices** and read the cache header in the **Terminal**.

<ul>
    {items.map((i: { name: string; price: number }) => <li>{i.name}: ₹{i.price}</li>)}
</ul>
`,
    "src/pages/account.astro": `---
// A personal page: never cache it.
Astro.response.headers.set("Cache-Control", "private, no-store");
---

<h1>Your account</h1>
`,
  }}
/>

- **[JS]** Leave out `: { name: string; price: number }`.

Astro is also testing built-in **route caching** behind an experimental flag. Experimental features can change between versions. Read the Astro docs for your version before using it on a real site.

**Simple lesson:** `Cache-Control` tells browsers and CDNs how long they may keep a page.

## Live Collections

### Build-Time vs Live Collections

- Content collections (Chapter 3) load data at **build time**. Great for posts.
- **Live collections** load data on **every request**. Great for stock, prices, scores.
- They live in `src/live.config.ts`, use `defineLiveCollection()`, and need an on-demand page.

<div className="tscroll">

| | Content collection | Live collection |
|---|---|---|
| Config file | `src/content.config.ts` | `src/live.config.ts` |
| Define with | `defineCollection()` | `defineLiveCollection()` |
| Loaded | Once, at build time | On every request |
| Read with | `getCollection()`, `getEntry()` | `getLiveCollection()`, `getLiveEntry()` |
| Errors | Stop the build | Returned as `error`, you handle them |

</div>

### A Live Loader

A live loader is an object with two functions:

- `loadCollection({ filter })` returns `{ entries: [...] }`.
- `loadEntry({ filter })` returns one `{ id, data }`, or nothing when it is not found.
- Either may return `{ error }` instead.

Press **▶ Run**. Then press **Refresh** a few times. The sticker stock goes down, because the data is loaded fresh on every request.

{error ? <p>Could not load stock: {error.message}</p> : (
    <ul>
      {entries.map((e) => <li>{e.data.name}: {e.data.left} left</li>)}
    </ul>
)}
<a href="/">Refresh</a>
`,
  }}
/>

- **[JS]** In `live.config.js`, leave out `interface Product` and the type annotations.

### Handling Errors

- Live data can fail: the API may be down, or an id may not exist.
- `getLiveCollection()` and `getLiveEntry()` **do not throw**. They return `{ error }`.
- Always check `error` before using `entries` or `entry`.

Press **▶ Run**. The mug page works. Then open **/products/hat**: there is no hat, so the page shows a friendly message instead of crashing.

) : (
    <>
      <h1>{entry.data.name}</h1>
      <p>₹{entry.data.price}, {entry.data.left} left</p>
    </>
)}
<p><a href="/products/mug">Mug</a> · <a href="/products/hat">Hat</a></p>
`,
  }}
/>

- **[JS]** Leave out the `!` after `Astro.params.id`.

**Simple lesson:** live collections load on every request. Always check `error` first.

## Choosing the Right Tool

<div className="tscroll">

| Situation | Best tool |
|---|---|
| Blog posts that change when you publish | Content collection, static pages |
| One personal or fast-changing box on a busy page | Server island |
| A whole page of always-fresh data | On-demand page + live collection |
| A page that can be a little old | On-demand page + `Cache-Control` |
| An interactive widget that reacts to clicks | Client island (Chapter 5) |

</div>

**Simple lesson:** static by default, a server island for one fresh part, live data for fully fresh pages.

## TL;DR

1. **Problem** = one fresh or personal part can make a whole page slow and uncacheable.
2. **Server island** = `<Comp server:defer />`. Rendered on the server, after the page. Needs an adapter.
3. **Fallback** = `<p slot="fallback">Loading…</p>` shows until the island arrives.
4. **Props** = encrypted in the island URL. Data only.
5. **Cookies** = server islands see the visitor's cookies. Personal parts on cached pages.
6. **Cache-Control** = `public, max-age=60, s-maxage=300` or `private, no-store`.
7. **Live collection** = `defineLiveCollection()` in `src/live.config.ts`. Loaded per request.
8. **Read live data** = `getLiveCollection()` / `getLiveEntry()`. Check `error` first.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| `server:defer` without an adapter | It works on a static host | It needs a server | Build error |
| No fallback | The island appears at once | It loads after the page | Empty space, then a jump |
| A function as a server island prop | Props can be anything | Props are encrypted data | Error |
| `Astro.url` inside the island for the page path | It is the page URL | It is the island URL | Wrong path; use the Referer header |
| Personal data in a CDN-cached page | Caches know who is who | A cache serves one copy to all | One visitor sees another's data |
| Using `entries` without checking `error` | Live data never fails | APIs fail | The page crashes |
| A live collection on a prerendered page | Live means build time too | Live is per request | Stale or no data |

</div>

## Check Yourself

## Exercises

## What to Remember

- One fresh or personal part should not slow down the whole page.
- `server:defer` renders a component later on the server. Add a `slot="fallback"` and an adapter.
- Server island props are encrypted. Islands see the visitor's cookies.
- `Cache-Control` decides how long browsers and CDNs keep a page.
- Live collections load data per request. Always check `error`.

Next: **Testing and Debugging**, where you check your site before visitors find the bugs.
