pagesizechecker.com

how-to

How JavaScript frameworks inflate HTML page size

Next.js, Nuxt, SvelteKit, Remix, and Astro all serialize state into your HTML. Here's where the bloat comes from in each, how big it gets, and what to do about it.

4 min read
Single figure walking forward with progressively faded duplicate silhouettes trailing behind, dissolving into particles

If you build with Next.js, Nuxt, SvelteKit, Remix, or any other modern meta-framework, your rendered HTML is bigger than you think. Often a lot bigger.

The reason is the same across all of them: hydration. To make a server-rendered page interactive on the client, the framework needs to send the data the page was rendered with. That data lives inline in the HTML, and on data-heavy pages it can dwarf the actual content. This post walks through where each framework hides its bloat and how to keep it under control.

The shared pattern: state serialization

Every server-side React, Vue, or Svelte framework follows the same flow. The server fetches data, renders HTML using that data, serializes the data into a <script> tag in the HTML, sends both to the browser, and then the client framework reads the serialized data and "hydrates" by re-attaching event handlers to make the page interactive.

The size problem lives in the serialization step. If your page renders a 100-item product grid, the framework usually includes all 100 items' worth of JSON in the HTML, even though that data is already represented as visible DOM elsewhere on the page. So the page pays the cost twice: once as rendered HTML, once as serialized state for hydration.

A figure straining forward, dragging an enormous cluster of stacked glowing geometric data shards tethered behind them by taut strings of cyan light

Next.js: __NEXT_DATA__ and React Server Components

In the Pages Router, every Next.js page includes a <script id="__NEXT_DATA__"> containing the full props returned from getServerSideProps or getStaticProps. A big query result means a big script tag:

<script id="__NEXT_DATA__" type="application/json">  {"props":{"pageProps":{...everything you returned from getServerSideProps...}}}</script>

The App Router with React Server Components changes the shape but not always the size. Server Components ship their serialized output as RSC payloads, which are essentially chunks of the React tree as JSON. These can still get very large for data-rich pages.

To mitigate on Next.js: in Pages Router, don't return data you don't need to client components from getServerSideProps. Only pass what hydration actually requires. In App Router, keep heavy data in Server Components, which don't ship their props to the client at all. Use streaming and Suspense to defer non-critical sections out of the initial HTML, and consider partial pre-rendering for routes where most of the content is static.

Nuxt: __NUXT__ payload

Nuxt 3 emits a window.__NUXT__ payload containing the page's state, useFetch and useAsyncData results, plus runtime config:

<script>window.__NUXT__ = (function(){...})()</script>

For most marketing pages this is fine. For dashboards and content-heavy routes, it adds up fast.

To mitigate on Nuxt: use useFetch(..., { server: false }) for data only needed client-side; it won't be serialized. Set up payload extraction (nuxt.config.tsexperimental.payloadExtraction) to move payloads to separate JSON files for static pages. And audit what useState, useAsyncData, and useFetch are storing globally, since anything in there gets serialized.

SvelteKit: data props and __sveltekit state

SvelteKit serializes the return values of every load function on the route and writes them to inline script tags. This is generally leaner than Next.js or Nuxt because SvelteKit doesn't ship a Redux-style global state out of the box, but heavy load returns still inflate HTML directly.

To mitigate on SvelteKit: slim down load returns. Send the IDs, fetch the bodies on the client when needed. Use +page.server.ts for data that shouldn't reach the client at all. For mostly-static pages, prerender = true shifts the work to build time.

Remix: __remixContext

Remix follows the same pattern. Every loader's return value is serialized into the HTML for hydration. The conventions differ a bit, but the size implications are the same.

To mitigate on Remix: loader returns are public to the client by design, so if you return 50 fields and only use 5, you're paying for 45. Use defer() to stream non-critical data after the initial render; streamed data doesn't appear in the initial HTML byte count.

Astro: islands architecture (mostly safe)

Astro is the exception. By default, Astro components ship zero JavaScript and zero serialized state. Hydration only happens for explicitly opted-in client components (client:load, client:idle, and friends).

The catch is that when you do opt a component into client-side rendering, Astro inlines its props as JSON, just like the others. A <MyChart client:load data={hugeDataset} /> will serialize hugeDataset into the HTML.

To mitigate on Astro: keep client:* components small and data-light by default. For heavy data, fetch on the client inside the component instead of passing it as a prop. Most pages can stay zero-JS, and that's Astro's biggest size advantage.

How to find the bloat

Open the page in DevTools and find the largest <script> tags in the Elements panel. The biggest ones are usually __NEXT_DATA__, __NUXT__, __remixContext, or an inline JSON script. Their size tells you how much hydration payload you're shipping.

For a more methodical look, see our guide to and the .

The general rule shows up across every framework: the data you fetch on the server gets shipped to the client as JSON, whether you use it or not. The fix is the same everywhere. Fetch less on the server, only what hydration actually needs. Push heavy data fetches to the client where they don't count toward initial HTML. Use streaming or partial pre-rendering where the framework supports it.

For a checklist beyond framework specifics, see . If your app is fully client-rendered, covers a related set of concerns.

Check your page size now

Test any URL against Google's 2MB Googlebot HTML limit in seconds.

Run a check