Vercel Logo

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():

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

In Nuxt, prefix the component name with Lazy:

<!-- 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:

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:

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:

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:

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.

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.

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

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.

Was this helpful?

supported.