DEV Community

Cover image for The Dark Side of useFetch and useAsyncData in Nuxt 3
Alex Marusych
Alex Marusych

Posted on

The Dark Side of useFetch and useAsyncData in Nuxt 3

When you build an app with Nuxt, Vue actually runs twice: once on the server and then again on the client. Because of that, if you fetch some data, it would normally happen two times too — once during server-side rendering, and then again when the page loads in the browser. To avoid this, Nuxt gives us useFetch and useAsyncData. Thanks to these built-in composables, you don’t have to worry about making duplicate requests. Everything feels smooth — you fetch data once, and it just works.

But have you ever thought about how this magic happens? In simple words, after the server fetches the data, Nuxt sneaks it into the HTML it sends to the client. It hides the response inside a script tag in the page’s body.

Payload for fetching the data in Nuxt3

Then, when Vue starts running on the client side, it grabs the data from there instead of making a second request. Pretty clever, right?

Payload for fetching the data in Nuxt3. Image 2

Let's say you're building a simple Nuxt application that fetches a book and displays it.

<template> <div class="container"> <div> <h2>Response</h2> <div class="response"> <pre>{{ data }}</pre> </div> </div> </div> </template> <script setup> const { data, error } = await useFetch('/api/book') </script> 
Enter fullscreen mode Exit fullscreen mode

When Vue runs on the server, useFetch requests the book and stores the response inside that hidden JSON. Later, when Vue runs on the client, useFetch just reads the saved data. No second request needed. Everyone's happy.

Payload for fetching the data in Nuxt3. Image 3

Payload for fetching the data in Nuxt3. Image 4

Well… almost everyone. There's one small catch. Since the data gets stuffed inside the HTML page, it makes the file heavier. If you need all the data - that's fine. But what if you don't? Imagine you only display three fields from the book: "name", "author" and "price".

<template> <div class="container"> <div> <h2>Response</h2> <div class="response"> <pre>{{ data }}</pre> </div> </div> <div> <h2>Card</h2> <div class="card"> <h3>{{ data.name }}</h3> <div class="author-price-block"> <span>{{ data.author }}</span> <span>{{ data.price }} USD</span> </div> </div> </div> </div> </template> 
Enter fullscreen mode Exit fullscreen mode

The server still fetched a huge object with tons of other information you don't even use. All of it still gets dumped into your HTML payload, making the page heavier for no good reason.

Payload for fetching the data in Nuxt3. Image 5

That's where a little extra care can make a big difference. Nuxt actually lets you control what ends up in the payload.

If your API returns an object, you can tell useFetch or useAsyncData to "pick" only the fields you need.

<template> <div class="container"> <div> <h2>Response</h2> <div class="response"> <pre>{{ data }}</pre> </div> </div> <div> <h2>Card</h2> <div class="card"> <h3>{{ data.name }}</h3> <div class="author-price-block"> <span>{{ data.author }}</span> <span>{{ data.price }} USD</span> </div> </div> </div> </div> </template> <script setup> const { data, error } = await useFetch('/api/book', { pick: ['name', 'author', 'price'] }) </script> 
Enter fullscreen mode Exit fullscreen mode

So even though the API still sends everything, only the important parts get saved into the HTML.

Payload for fetching the data in Nuxt3. Image 6

If you're dealing with a list - like an array of books - you can go even further and "transform" the whole response.

<template> <div class="container"> <div> <h2>Response</h2> <div class="response"> <pre>{{ data }}</pre> </div> </div> <div> <h2>Cards</h2> <div> <div v-for="item in data" :key="item.id" class="card" > <h3>{{ item.name }}</h3> <div class="author-price-block"> <span>{{ item.author }}</span> <span>{{ item.price }} USD</span> </div> </div> </div> </div> </div> </template> <script setup> const { data, error } = await useFetch('/api/books', { transform: (data) => data.map(item => ({ id: item.id, name: item.name, author: item.author, price: item.price })) }) </script> 
Enter fullscreen mode Exit fullscreen mode

This way you can completely control the size of the payload, no matter how much data the API sends.

Payload for fetching the data in Nuxt3. Image 7


So here's the thing: even though useFetch and useAsyncData protect you from double-fetching, they can still quietly hurt your performance if you're not careful with the payload. It's always a good habit to check what actually ends up inside the page.

Top comments (0)