How to Fix: I hope more people can participate in the discussion of this topic !, How to render on the server without returning hydrated data next? This will increase the size and volume of HTML and document files

7 min read

Next.js server rendering still ships hydration data because React must rebuild the component tree on the client.

If your goal is to render on the server without sending large hydrated payloads like __NEXT_DATA__ or React Flight data, the key point is this: in classic Next.js pages and in any interactive React tree, hydration metadata is not optional. It is the mechanism that lets the browser attach event handlers, resume component state, and continue navigation without a full reload. Trying to remove it while keeping the same client-side behavior is what causes confusion in discussions like this GitHub issue.

Understanding the Root Cause

Next.js supports multiple rendering models, but they do not all behave the same way:

  • SSR in the Pages Router renders HTML on the server, then sends hydration data so React can take over in the browser.
  • SSG/ISR also typically includes serialized page data for hydration.
  • App Router with Server Components reduces client JavaScript, but still sends the data required to reconstruct any client boundaries.
  • Pure static HTML can avoid hydration, but then there is no React interactivity on the client.

That is why HTML and document payload size can grow. The server response contains not only rendered markup, but also the data needed for React to resume. In older page-based rendering, this usually appears in a script payload. In the App Router, it may appear as React Server Component flight data plus client bundles for interactive components.

So the technical answer to “How do I render on the server without returning hydrated data?” is:

  • If the page must be hydrated, you cannot fully remove that data.
  • If you want to avoid hydration payloads, you must move toward non-interactive HTML or minimize client components.

The optimization target is usually not “remove all hydration,” but rather:

  • reduce the amount of serialized props,
  • reduce the number of client components,
  • avoid duplicating large datasets in HTML and JSON payloads,
  • stream server-rendered content where possible.

Step-by-Step Solution

The right solution depends on which Next.js architecture you are using.

1. If you are using the App Router, prefer Server Components by default

In modern Next.js, files under app/ are Server Components unless you add ‘use client’. This is the best path if you want less hydration overhead.

// app/page.tsx
import ProductList from './ProductList'

export default async function Page() {
  const res = await fetch('https://example.com/api/products', {
    cache: 'no-store'
  })
  const products = await res.json()

  return (
    <main>
      <h1>Products</h1>
      <ProductList products={products} />
    </main>
  )
}
// app/ProductList.tsx
// No 'use client' here
export default function ProductList({ products }) {
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  )
}

This component tree stays on the server and avoids shipping unnecessary client JavaScript. However, if you later add a button with onClick, that part must become a Client Component, which reintroduces hydration for that boundary.

2. Isolate interactivity into small Client Components

Do not mark an entire page as ‘use client’ if only one widget needs browser interaction.

// app/page.tsx
import Filters from './Filters'
import InteractiveSort from './InteractiveSort'

export default async function Page() {
  const products = await getProducts()

  return (
    <section>
      <Filters />
      <InteractiveSort items={products.map(p => ({ id: p.id, name: p.name }))} />
    </section>
  )
}
// app/InteractiveSort.tsx
'use client'

import { useState } from 'react'

export default function InteractiveSort({ items }) {
  const [ascending, setAscending] = useState(true)
  const sorted = [...items].sort((a, b) =>
    ascending ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
  )

  return (
    <div>
      <button onClick={() => setAscending(!ascending)}>
        Toggle sort
      </button>
      <ul>
        {sorted.map(item => <li key={item.id}>{item.name}</li>)}
      </ul>
    </div>
  )
}

This approach keeps hydration limited to the smallest possible subtree.

3. Stop sending oversized props to the client

A common reason document size grows is that developers pass large API responses directly into interactive components. Only send the fields the client really needs.

// Bad: large object graph passed to a client component
const products = await getProducts()
return <InteractiveSort items={products} />
// Better: trim the payload
const products = await getProducts()
const lightweightItems = products.map(p => ({
  id: p.id,
  name: p.name
}))
return <InteractiveSort items={lightweightItems} />

This reduces both serialized payload size and hydration cost.

4. If you use the Pages Router, understand the limitation

In the pages/ router, SSR always expects hydration if the page is a React application in the browser. Example:

// pages/index.tsx
export async function getServerSideProps() {
  const res = await fetch('https://example.com/api/posts')
  const posts = await res.json()

  return {
    props: {
      posts
    }
  }
}

export default function Home({ posts }) {
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  )
}

This model includes serialized props so React can hydrate. If you truly need HTML only, you would need to avoid client-side React behavior for that route, or serve content outside the normal hydrated page model.

5. For content-only pages, prefer static or server-only output

If a page is mostly documentation, blog content, legal text, or other non-interactive UI, keep it server-rendered and avoid client widgets. In the App Router, that means:

  • no ‘use client’ unless necessary,
  • no large browser state trees,
  • minimal client-side libraries.

If the page needs zero interactivity, this gives you behavior much closer to plain HTML delivery.

6. Use dynamic imports for optional client features

If a browser-only feature is not required for first paint, load it lazily.

import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('./Chart'), {
  ssr: false
})

export default function Page() {
  return (
    <div>
      <h1>Analytics</h1>
      <Chart />
    </div>
  )
}

This does not eliminate hydration globally, but it can reduce initial document and execution cost.

7. Verify what is increasing your payload

Before changing architecture, inspect whether the problem is caused by:

  • large JSON props,
  • too many Client Components,
  • embedded CMS content duplicated in multiple places,
  • large third-party libraries,
  • unnecessary browser state initialization.

Use browser devtools, the Next.js bundle analyzer, and page source inspection to confirm what is actually being sent.

Common Edge Cases

Mixing Server Components and Client Components incorrectly

If you accidentally add ‘use client’ high in the tree, a large section of your page becomes client-rendered. That increases JavaScript and hydration data significantly.

Passing non-serializable data

Objects like functions, class instances, Dates without normalization, Maps, or complex prototypes can break serialization between server and client boundaries. Normalize data before passing it.

const safeData = items.map(item => ({
  id: String(item.id),
  createdAt: item.createdAt.toISOString()
}))

Expecting zero hydration on interactive pages

If your page includes event handlers, local state, client-side routing logic, animations driven by React, or browser APIs, some hydration is required. That is a platform constraint, not just a Next.js setting.

Large CMS or markdown pages duplicated in payloads

If full article content is rendered into HTML and also passed as client props to a client component wrapper, document size can nearly double. Keep the rich content in a Server Component and avoid passing it through a client boundary.

Using getServerSideProps for data-heavy pages

In the Pages Router, very large responses returned from getServerSideProps are serialized into the page payload. Consider moving to the App Router or reducing the prop shape.

Confusing SSR with no-JS rendering

Server-side rendering does not automatically mean no hydration. SSR means HTML is produced on the server. Hydration is the separate step that makes that HTML interactive in the browser.

FAQ

Can I disable __NEXT_DATA__ completely in a normal Next.js page?

Not if that page is expected to hydrate as a React app on the client. In the Pages Router, that payload is part of how Next.js restores props and bootstraps the app. In the App Router, the format differs, but client boundaries still require data transfer.

What is the best way to reduce document size in Next.js?

Use Server Components by default, minimize Client Components, trim props before passing them to the client, avoid wrapping large server-rendered content in client components, and lazy-load optional browser-only features.

Is there any way to get server-rendered HTML with no hydration at all?

Yes, but only if the page is effectively non-interactive from React’s perspective. That means plain server-rendered content, static output, or architecture where React does not need to attach event handlers in the browser.

The practical conclusion for this issue is simple: you cannot keep full React interactivity and also remove hydration data entirely. The real fix is to reduce the hydration surface area by moving rendering to the server, shrinking client boundaries, and sending only the minimal data needed for interactive islands. If you are currently on the Pages Router and payload size is a recurring problem, migrating critical routes to the App Router is usually the most effective long-term improvement.

Leave a Reply

Your email address will not be published. Required fields are marked *