Glass Box Terrarium, explained from the inside

Last updated · Latest source on GitHub ↗

Nuxt & VueLesson

Data fetching

A page that renders on the server and then comes alive in the browser runs its setup code twice. Load data in setup the plain way and it is fetched twice too: once for the HTML, once more in the browser. Nuxt's useAsyncData fetches once and sends the result along. With a page cache, Nuxt can also send it as a file when the visitor clicks to the next page.

Know useAsyncData and payloads already? Skip to In Terrarium.

Outside the box · General

The concept

useAsyncData(key, handler) runs handler, an async function, and returns refs: data holds its result, status and error say how it went. On the server, Nuxt waits for the handler before it renders the HTML, then stores the result under key in the payload: the data Nuxt serializes into the page for the browser. When the browser takes over the page, useAsyncData finds its key in the payload and doesn't run the handler again. Calls with the same key share one result, so the key must be unique to the data. Without one, Nuxt makes a key from the call's place in the source.

That covers the first page. When the visitor follows a link, the browser builds the next page itself, and normally runs that page's handlers there. Payload extraction changes that for some routes: Nuxt serves the payload of a server render at a _payload.json URL next to the route, and on a client navigation fetches it and uses its data instead of running the handlers. With experimental.payloadExtraction: 'client', the first page keeps its payload inline in the HTML, and only navigations fetch _payload.json.

Only some routes get one: a prerendered route has it written to a file at build, and a route the server caches renders it on request, like the page itself. A route is cached when routeRules in nuxt.config.ts gives it a cache rule, such as { cache: { maxAge: 60 } }; the Nitro server engine applies it.

Full referenceNuxt docs: useAsyncData

Looking inside

In Terrarium

Terrarium's pages load their content inside useAsyncData, most of them keyed by the route's path. The Journal's Document page loads its Document under that key, then the steps of its onramp tour under a second one:

layers/journal/app/pages/t/journal/[space]/[...slug].vue ↗21–43as of
21const { data: page, error } = await useAsyncData(route.path, () =>
22  queryCollection(pagesKey).path(path).first(),
23)
⋯// …
34const { data: tour } = await useAsyncData(`${route.path}:onramp`, async () =>
35  page.value?.onramp == null
36    ? []
37    : onrampSteps(
38        await queryCollection(pagesKey)
39          .where('onramp', 'IS NOT NULL')
40          .select('path', 'onramp', 'onrampLabel', 'onrampBlurb')
41          .all(),
42      ),
43)

queryCollection is Nuxt Content's query. On the server it reads the server's SQLite database. In the browser it downloads a SQLite runtime compiled to WebAssembly, and a dump of each Collection it queries, the first time it runs. The Platform's config turns on payload files:

nuxt.config.ts ↗14–19as of
14  // In-app navigation reads a server-rendered `_payload.json` instead of
15  // querying Content in the browser (ADR-0028).
16  experimental: {
17    payloadExtraction: 'client',
18    defaults: { nuxtLink: { prefetchOn: { visibility: false, interaction: true } } },
19  },

The same config gives the home page and every Tenant page a short cache rule, so each has a payload. A link fetches its route's code and payload when the pointer moves onto it, a finger touches it or the keyboard focuses it, not when it scrolls into view. On the click, useAsyncData finds its key in the fetched payload, the handler never runs, and the browser loads no database.

Tinkerfund's search route is not cached and has no payload: after a click to it, its useAsyncData handler runs in the browser and queries the browser database (its search page is in layers/tinkerfund/app/pages/t/tinkerfund/).

Why it's built this way

  • Payloads over browser queries. Before payloads, the first click downloaded a database engine and each Collection the next page queried, and a failed download left a section empty. Now a click fetches one small payload the server renders and caches, several to dozens of times lighter. The cost: the browser database still loads for an uncached page or a failed payload fetch. ADR-0028 (docs/adr/0028-navigation-reads-server-payloads.md).
  • Documented settings only. The other options either broke a Tenant's shop, which keeps a cart in the browser, or relied on undocumented Nuxt Content internals that break on upgrades. Payloads use only documented Nuxt settings. ADR-0028 (docs/adr/0028-navigation-reads-server-payloads.md).
  • Cache, not prerender. Nuxt serves a payload only for pages it prerenders or caches. Prerendering every page would lengthen the build, need a kept-up address list, and miss query strings. Content changes only with a new build, so the server caches pages briefly instead. The cost: one cached copy serves every visitor, which Nitro server engine shows how to handle. ADR-0028 (docs/adr/0028-navigation-reads-server-payloads.md).
  • Fetch on interaction. Nuxt's default fetches every visible link's payload ahead of time, and one page fetched the same big payload many times, making page changes heavier than before payloads. Fetching only on hover, touch or focus fixed that. The cost: the next page's code loads later. ADR-0028 (docs/adr/0028-navigation-reads-server-payloads.md).

What to watch for

  • Return what the page reads. A handler that returns whole Documents sends every field. The Blog's front page, layers/blog/app/pages/t/blog/index.vue, .select()s only the fields it lists.
  • One key per piece of data. A page with two useAsyncData calls needs two keys, as in the Journal's page above, or the second gets the first's result.
  • A query outside useAsyncData still loads the browser database. A payload stands in only for useAsyncData results on a cached route. Tinkerfund's search field queries as the visitor types (layers/tinkerfund/app/components/tinkerfund/shell/SearchField.vue), so its first suggestion downloads the database, however light the page itself was (Content database says what that costs).

Try it in your project

Take a page that loads its data in useAsyncData, and give its route a short cache and payload extraction:

export default defineNuxtConfig({
  experimental: { payloadExtraction: 'client' },
  routeRules: { '/blog/**': { cache: { maxAge: 60, swr: false } } },
})

Build and start the app (nuxt build, then nuxt preview). Open another page, then click a link to the cached one with the browser's network panel open: you should see its _payload.json and no request from the handler.

Where next

Walkthroughs through this Lesson

Last updated · Latest source on GitHub ↗

Something went wrong

This content couldn’t be loaded. Reloading the page usually fixes it.

Technical details
route: /t/glassbox/guide/nuxt-vue/use-async-data