How to Reduce Nuxt Payload Size
This guide is part of Vue & Nuxt Performance, within Framework Performance. When Nuxt renders a page on the server, the results of useFetch and useAsyncData (and state from useState and stores) are serialised into the HTML so the client can hydrate without fetching again. This payload is convenient and invisible — until it is hundreds of kilobytes. A product listing that fetches full product records, a blog index that fetches every post's full body, or a page that stores a whole API response in a store can ship more payload than HTML markup.
A large payload costs twice. It makes the HTML larger, delaying the arrival of content and critical resources referenced later in the document. And the client must parse and revive it into reactive state during hydration, adding main-thread time before the page responds to input.
Rapid Diagnosis
- View source and find the
__NUXT_DATA__script; measure its size. - Compare payload fields with rendered fields: fields fetched but not displayed are waste.
- Check prerendered routes'
_payload.jsonfiles in the build output. - Check stores (Pinia) populated during SSR; their state is serialised too.
Root Cause Analysis
1. Whole API responses. Endpoints return everything; pages use a fraction.
2. Data for hidden UI. Tabs, modals and below-the-fold sections fetched during SSR.
3. Duplicated state. The same data in a composable and a store.
4. Large nested structures. Reviews, variants and related items embedded in each record.
Step-by-Step Resolution
1. Pick or transform fetched data
const { data: posts } = await useAsyncData('posts', () => $fetch('/api/posts'), {
transform: (list) => list.map(({ slug, title, excerpt, date }) => ({ slug, title, excerpt, date })),
});
// trade-off: transform runs on the server before serialisation, but the server still
// downloads the full response; trimming at the API is better when you control it.
2. Return lean data from server routes
Create Nuxt server routes (server/api/*.ts) that query or reshape upstream data and return only the fields pages need.
3. Fetch non-critical data lazily on the client
const { data: reviews, pending } = useFetch(`/api/products/${id}/reviews`, { lazy: true, server: false });
// trade-off: server: false keeps reviews out of the payload and SSR HTML; they render
// after hydration, so reserve space to avoid layout shifts and accept no SSR for them.
4. Avoid duplicating state
Store data once — either in the composable result or a store — and derive views with computed properties.
NUXT_DATA
Verification
Measure the payload again on the same pages: it should shrink substantially. Compare HTML transfer size, hydration time in a throttled trace, and field LCP and INP for the routes. Check that pages still render correctly when navigating client-side (data shape changes can break components).
Worked Example: A Recipe Platform
A recipe platform's category pages fetched 30 full recipes (ingredients, steps, nutrition, comments) to render cards with a title, image, rating and cooking time. The payload was 520KB per page, and hydration took 700ms on mid-range phones. A server route returning card fields only reduced the payload to 22KB. Comments were already loaded lazily on recipe pages but were also fetched during SSR on category pages through a shared composable, which was fixed. HTML transfer fell by 80%, LCP p75 improved by 400ms (the hero image reference arrived earlier), and hydration dropped to 160ms.
Payload Extraction for Prerendered Routes
For prerendered and cached routes, Nuxt can extract the payload into a separate _payload.json file. On client-side navigation to those routes, only the JSON is fetched — not the whole HTML — and it can be cached. This reduces navigation cost for static content. The HTML still includes inline payload for the first load. Payload extraction is enabled by default for prerendered routes in recent Nuxt versions; check that it applies to your SWR or ISR routes if you rely on them.
Measuring Payload in CI
Payload growth is gradual: a new field here, a related-items list there. Add a CI check that renders key pages (or reads prerendered output) and asserts payload size stays under a budget. A simple script can fetch the page from a preview deployment, extract the __NUXT_DATA__ script and compare its byte length against limits per template. Reporting the top keys by size makes it easy to see which data grew.
// scripts/payload-budget.mjs
const budgets = { '/': 30_000, '/blog/': 40_000, '/products/shoes/': 60_000 };
for (const [path, max] of Object.entries(budgets)) {
const html = await (await fetch(process.env.PREVIEW_URL + path)).text();
const m = html.match(/<script[^>]*id="__NUXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
const size = m ? m[1].length : 0;
if (size > max) { console.error(`${path}: payload ${size} B > ${max} B`); process.exitCode = 1; }
}
// trade-off: byte length of the serialised script approximates its cost; HTML is
// compressed in transfer, but parsing and reviving scale with the raw size.
Common Mistakes
- Trusting that pick handles nesting.
pickworks on top-level keys; usetransformfor deeper shaping. - Fetching below-the-fold data on the server. It goes into the payload and delays HTML.
- Large store state. Pinia state populated during SSR is serialised in full.
- Changing data shapes without updating components. Client navigations break.
Edge Cases
SEO-critical content. Data needed for indexable content must be rendered on the server; trim fields, do not defer them.
Dates and special types. Serialisation handles many types, but custom classes lose their prototypes; transform to plain data.
Large static data. Constants (country lists, config) can be imported in code instead of fetched and serialised.
Payload on error pages. Errors can serialise unexpected data; check error page payloads too.
FAQ
What is __NUXT_DATA__?
The script element containing the serialised payload Nuxt uses to hydrate the client without refetching data.
Does payload size affect LCP?
It can. A large payload makes the HTML larger, delaying content and resource references that come after it, and adds hydration work.
What is the difference between pick and transform?
pick keeps selected top-level keys of the result. transform runs a function on the result, allowing any reshaping.
Should I use server: false for all non-critical data?
For data below the fold or behind interactions, often yes. It removes the data from SSR and the payload, at the cost of rendering it after hydration.
Is Pinia state part of the payload?
Yes, state populated during server rendering is serialised for hydration. Keep store state lean.
How large should a payload be?
As small as the page allows — tens of kilobytes for content pages is reasonable. Hundreds of kilobytes usually signals overfetching.
Does payload extraction help the first load?
No. It helps client-side navigations to prerendered routes by fetching a small JSON file instead of the full HTML.
Can I compress the payload further?
The HTML is compressed in transfer already. Reducing the data itself matters more, because parsing and reviving costs scale with raw size.
Can I see payload contents in development?
Yes. Nuxt DevTools shows payload and state contents, which makes it easy to spot oversized data during development.
Do prerendered pages still include a payload?
Yes, inline for the first load and, with payload extraction, as a separate JSON file for client-side navigations.
Does useState add to the payload?
Yes. State created with useState during server rendering is serialised; keep it small.
Related
- Lazy hydration in Nuxt — less hydration work.
- Nuxt route rules for hybrid rendering — caching payload-heavy routes.
- Vue & Nuxt performance — the topic overview.