---
title: "Optimization"
description: "Optimize the hot springs app with lazy-loaded components, server route caching, and payload reduction techniques."
canonical_url: "https://vercel.com/academy/nuxt-on-vercel/optimization"
md_url: "https://vercel.com/academy/nuxt-on-vercel/optimization.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-10-16T17:45:50.612Z"
content_type: "lesson"
course: "nuxt-on-vercel"
course_title: "Nuxt on Vercel"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Optimization

# Optimization

The current dataset has 17 springs, but larger datasets and heavier traffic change the performance profile. Measure the app first, then optimize the parts that matter.

This lesson applies lazy-loaded components, server route caching, and payload reduction through small configuration and code changes.

## Outcome

Optimize the app with lazy loading, caching, and leaner payloads.

## Fast Track

1. Prefix a component with `Lazy` for on-demand loading
2. Add `defineCachedEventHandler` for server route caching
3. Reduce the payload by selecting only needed fields

## Hands-on exercise 5.3

Apply three optimization techniques to the hot springs app.

**Requirements:**

1. Use `LazySpringCard` on the browse page to lazy-load the card component
2. Add caching to the springs list server route with `defineCachedEventHandler`
3. Create a lightweight springs list endpoint that returns only the fields needed for cards

**Implementation hints:**

- Any component can be lazy-loaded by prefixing its name with `Lazy`. `<SpringCard>` becomes `<LazySpringCard>`. No import changes, no config
- `defineCachedEventHandler` wraps a handler with a server-side cache. Set `maxAge` in seconds
- For the browse page, cards only need: id, name, description (truncated), location, temperature, type, and elevation. The full description, features array, and imageUrl aren't needed in the list view

**Lazy components:**

In Next.js, you lazy-load components with `dynamic()`:

```tsx
const SpringCard = dynamic(() => import("./SpringCard"));
```

In Nuxt, prefix the component name with `Lazy`:

```tsx
<!-- Eager (loads with the page) -->
<SpringCard :spring="spring" />

<!-- Lazy (loads when needed) -->
<LazySpringCard :spring="spring" />
```

Nuxt generates the lazy wrapper at build time without `dynamic()` or a Suspense boundary. The component loads when it first renders. In a grid of spring cards, code for components below the fold does not block the initial render.

Update the browse page template:

```tsx title="app/pages/springs/index.vue: template change"
<div v-else-if="springs?.length">
  <LazySpringCard v-for="spring in springs" :key="spring.id" :spring="spring" />
</div>
```

The `Lazy` prefix is the only template change.

**Server route caching:**

The springs data comes from a JSON file that doesn't change at runtime. We can cache the API response:

```typescript title="server/api/springs/index.get.ts: with caching"
import type { Spring } from "~/types/spring";
import springs from "~/server/data/springs.json";

export default defineCachedEventHandler(
  (event) => {
    const query = getQuery(event);

    let results = springs as Spring[];

    if (query.region && typeof query.region === "string") {
      results = results.filter(
        (s) =>
          s.location.region.toLowerCase() ===
          query.region!.toString().toLowerCase()
      );
    }

    if (query.type && typeof query.type === "string") {
      results = results.filter((s) => s.type === query.type);
    }

    if (query.search && typeof query.search === "string") {
      const term = query.search.toLowerCase();
      results = results.filter(
        (s) =>
          s.name.toLowerCase().includes(term) ||
          s.description.toLowerCase().includes(term)
      );
    }

    return results;
  },
  {
    maxAge: 60 * 60, // 1 hour
    varies: ["x-query"],
  }
);
```

`defineCachedEventHandler` wraps the handler with Nitro's built-in caching layer. The response is cached for one hour. Different query parameters get different cache entries (the `varies` option). After an hour, the next request regenerates the cache.

In Next.js, you'd achieve similar caching with `export const revalidate = 3600` on a page, or `unstable_cache` for server-side data, or by setting `Cache-Control` headers manually on Route Handler responses. Nuxt puts the caching decision on the server route itself, which means the same cache applies regardless of which page calls it.

For our static JSON data, you could set `maxAge` to a day or more. For a real database, you'd balance freshness against load.

**Payload optimization:**

When the browse page loads via SSR, `useFetch` serializes the entire API response into the HTML payload. Seventeen springs with full descriptions, features arrays, and coordinates adds up. The cards only need a subset of fields.

You can use `pick` on `useFetch` to select only the fields you need:

```typescript title="app/pages/springs/index.vue: optimized fetch"
const { data: springs, status } = useFetch("/api/springs", {
  query: queryParams,
  pick: ["id", "name", "description", "location", "temperature", "type", "elevation"],
});
```

There's a catch: `pick` works on the top level of the response object, but our data is an array. For array responses, use `transform` instead:

```typescript title="app/pages/springs/index.vue: with transform"
const { data: springs, status } = useFetch("/api/springs", {
  query: queryParams,
  transform: (data) =>
    data.map(({ id, name, description, location, temperature, type, elevation }) => ({
      id,
      name,
      description,
      location,
      temperature,
      type,
      elevation,
    })),
});
```

The `transform` function runs after the fetch and before the data is serialized into the payload. It strips out `features` and `imageUrl`, which the card component doesn't use. For 17 springs, the savings are small. For 1,700, they're significant.

\*\*Note: Measure before you optimize\*\*

Nuxt DevTools shows serialized data size in the Payload tab. Compare it before and after the change to verify that the payload decreased.

\*\*Warning: Caching and authentication\*\*

Don't cache routes that return user-specific data. Our favorites and visited routes should NOT use `defineCachedEventHandler` because the response depends on who's logged in. The springs list is safe to cache because it's the same for everyone.

## Try It

1. Update the browse page to use `LazySpringCard`. The page should still work the same way, but check the Network tab. The SpringCard chunk should load separately
2. Add caching to the springs list route. Make two requests to `/api/springs`. The second should be faster (served from cache)
3. Add the `transform` to the browse page fetch. Check the Payload tab in Nuxt DevTools to see the reduced data

## Commit

```bash
git add -A && git commit -m "feat(perf): add lazy loading, caching, and payload optimization"
```

## Done-When

- [ ] The browse page uses `LazySpringCard` for lazy-loaded cards
- [ ] The springs list API route uses `defineCachedEventHandler` with a 1-hour TTL
- [ ] You can explain when to use `transform` vs `pick` with `useFetch`
- [ ] You know which routes should NOT be cached (user-specific ones)

## Solution

The code blocks above contain the lazy component prefix, cached event handler, and transform function used in this lesson.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
