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.
Then, when Vue starts running on the client side, it grabs the data from there instead of making a second request. Pretty clever, right?
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>
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.
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>
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.
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>
So even though the API still sends everything, only the important parts get saved into the HTML.
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>
This way you can completely control the size of the payload, no matter how much data the API sends.
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)