---
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-05-03T01:41:34.223Z"
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 app works. Pages load, filters filter, reviews submit. But we haven't thought about what happens when the dataset grows from 17 springs to 1,700, or when the server route gets hit 10,000 times a minute. Optimization in Nuxt follows the same principle as optimization everywhere: measure first, fix what matters, skip what doesn't.

We'll cover three techniques that apply to almost every Nuxt app: lazy-loaded components, server route caching, and payload optimization. None of these require a rewrite. They're configuration changes and small code tweaks.

## 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" />
```

No import, no `dynamic()`, no Suspense boundary. Nuxt generates the lazy wrapper at build time. The component loads when it enters the viewport or when it's first rendered. For a grid of 17 spring cards, lazy loading means the cards below the fold don't 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>
```

One prefix. That's the 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 has a Payload tab that shows the size of serialized data. Check it before and after optimization to verify you're actually reducing the payload. Premature optimization is still the root of all evil, even in Nuxt.

\*\*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

See the code blocks above for the lazy component prefix change, the cached event handler, and the transform function. The changes are minimal: one prefix, one wrapper function, and one transform.


---

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