How to optimize your document size in Next.js

A quick reference checklist to help you optimize your Next.js document size for better performance and lower costs.

7 min read
Last updated February 5, 2026

Large HTML files slow down your site. When your HTML is too big, your Time to First Byte (TTFB) increases. Since nothing can happen on the frontend until the browser receives that first byte, a slow TTFB delays every other loading metric, including your Largest Contentful Paint (LCP).

But there's another problem: browsers can't start loading your CSS, JavaScript, images, or fonts until they've downloaded and parsed the HTML. The larger your HTML, the longer everything else has to wait. A 500kb HTML file doesn't just delay your initial render. It delays everything.

This checklist helps you find and fix the most common causes of bloated HTML in Next.js.

This is a critical step for reducing your HTML size. When you pass data from a Server Component to a Client Component, Next.js includes that data in the initial HTML as part of the RSC payload. This payload is sent twice: once embedded in the HTML for the initial load, and again as a separate fetch during client-side navigation.

  • Pass only what you need: If you're only using a user's name, pass user.nameinstead of the whole user object. Database records include metadata, timestamps, and internal IDs that your UI doesn't need. Extract just the fields your component uses.
  • Use React.cache(): This helps deduplicate data fetching across your component tree. It ensures you don't fetch and serialize the same data multiple times if different components need the same information.
// Bad: Passing entire user object (includes metadata, timestamps, etc.)
<ClientComponent user={fullUserObject} />
// Good: Pass only what's needed
<ClientComponent userName={user.name} userAvatar={user.avatar} />

Developers have seen their RSC payload drop from 250kb to just 5-15kb by being selective with their data. This directly translates to faster page loads and less data over the wire.

Reference: How to optimize RSC payload size

Inline SVGs are convenient because you can style them with CSS easily. However, they live directly in your HTML. Since SVGs are XML-based, they can be surprisingly large, especially for complex illustrations or detailed icons.

  • Use the Image component: Move your SVGs to the public folder and use the Next.js <Image> component. Set the unoptimized prop since SVGs are vector-based and don't need resizing. This allows the browser to cache each SVG file separately from your HTML. When you use the same logo across multiple pages, it loads once and gets reused everywhere. On Vercel, these files are automatically cached on the global CDN.
  • Watch out for complex paths: A single complex SVG can add several kilobytes to every page it appears on. Footer icons and other below-the-fold SVGs don't need to load immediately. With the Image component, you get automatic lazy loading. The browser only fetches these files when the user scrolls near them. With inline SVGs, the XML is always in your HTML, blocking the initial render even for content the user might never see.
// Bad: Inline SVG bloats HTML
export function Logo() {
return (
<svg width="100" height="100">
<path d="M10 10 H 90 V 90 H 10 Z" />
</svg>
)
}
// Good: External SVG with Image component
import Image from 'next/image'
export function Logo() {
return <Image src="/logo.svg" width={100} height={100} alt="Logo" unoptimized />
}

Reference: Next.js Image component

The way you handle styles affects how much extra weight is added to your HTML. Some modern CSS frameworks are much better for HTML size than others.

  • Prefer CSS Modules or Tailwind: These approaches generate static CSS files that are cached by the browser. They don't add extra weight to your HTML for every component instance. Tailwind keeps your CSS bundle small regardless of how many components you have, unlike runtime CSS-in-JS approaches.
  • Be careful with runtime CSS-in-JS: Libraries like styled-components or Emotion often inject <style> tags into your HTML at runtime. This can lead to significant bloat, especially on pages with many styled elements. Every time a component renders, it might be adding more style information to the document.
  • Look into zero-runtime alternatives: If you love the CSS-in-JS developer experience, consider libraries that extract styles into static CSS files during the build process. This gives you the best of both worlds: a great developer experience and optimized HTML.
// Runtime CSS-in-JS (adds to HTML)
import styled from 'styled-components'
const Button = styled.button`
background: blue;
padding: 10px;
`
// Better: CSS Modules (static CSS file)
import styles from './button.module.css'
export function Button() {
return <button className={styles.button}>Click</button>
}
// Or Tailwind (utility classes)
export function Button() {
return <button className="bg-blue-500 px-4 py-2">Click</button>
}

Server Components don't add their code to your client-side bundle, and they help keep your HTML cleaner by doing the heavy lifting on the server.

  • Only use 'use client' when necessary: Reserve client components for parts of your UI that need interactivity, like forms, buttons, or state. If a component just displays data, keep it as a Server Component.
  • Push client components down the tree: Keep the bulk of your page as Server Components and only make the small, interactive leaves Client Components. This minimizes the amount of data that needs to be serialized across the "client boundary."
  • Pass Server Components as children: You can wrap Server Components inside Client Components by passing them as children. This is a powerful pattern that lets you keep the inner components on the server while the outer component handles client-side logic.

Server Components run on the server and have no impact on your client-side JavaScript bundle. This means you can keep your client bundle smaller by only using Client Components where you need interactivity.

// Bad: Everything is a client component
'use client'
export function ProductPage({ product }) {
const [quantity, setQuantity] = useState(1)
return (
<div>
<ProductDetails product={product} />
<Reviews reviews={product.reviews} />
<QuantitySelector value={quantity} onChange={setQuantity} />
</div>
)
}
// Good: Only interactive part is client component
export function ProductPage({ product }) {
return (
<div>
<ProductDetails product={product} />
<Reviews reviews={product.reviews} />
<QuantitySelector /> {/* This is the only client component */}
</div>
)
}

Reference: Next.js Server Components

Streaming doesn't technically reduce the total HTML size, but it changes how that HTML is delivered to the user. This has a huge impact on perceived performance.

  • Wrap slow components in Suspense: This allows Next.js to send the "shell" of your page immediately while the slower parts load in the background. The user sees the layout and navigation right away instead of waiting for the whole page to be ready.
  • Stream content instead of blocking: By using streaming, you improve the perceived performance because the user sees content sooner. Next.js sends the HTML in chunks as it's generated, which is much better than waiting for a single massive response.
  • Prioritize critical content: Use Suspense boundaries to ensure that the most important parts of your page (like the hero section) are delivered first, while less critical parts (like a related products list) load later.
// Bad: Page waits for slow data
export default async function Page() {
const slowData = await fetchSlowData() // Blocks entire page
return <SlowComponent data={slowData} />
}
// Good: Stream with Suspense
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>
)
}

Reference: Streaming and Suspense

You can't fix what you don't measure. Use these tools to see exactly what's taking up space in your HTML and RSC payload.

  • Check the RSC payload: After a build, you can find RSC files in your .next folder for statically rendered pages. Run this command to see the largest ones: find .next -type f -name "*.rsc" -exec du -h {} + | sort -nr | head -n 5. Note that dynamic pages won't appear here. For those, use browser DevTools.
  • Use browser DevTools: Open the Network tab and look for requests ending in ?_rsc=. These show you the data being sent to your client components during navigation. You can inspect the response body to see exactly what's being serialized.
  • Try the RSC Parser: Tools like the RSC Parser give you a visual breakdown of what's inside your payload. It's much easier to spot a giant JSON object when it's visualized.
  • Watch for the 128kB warning: Next.js will show a warning in your console if your page data exceeds 128kB. This is a clear signal that you need to optimize your data fetching or component structure.
// In your browser DevTools Network tab:
// 1. Filter by "Fetch/XHR"
// 2. Look for requests ending in ?_rsc=...
// 3. Click to see the payload size and contents
// Example of what you might find:
// Request: /products?_rsc=abc123
// Size: 245 KB ← This is too large!
// Response: [["$","div",null,{"children":[...]}]]

Reference: Large page data warning

  • Filter your data before passing it to client components.
  • Move SVGs out of your components and into the public folder.
  • Use Tailwind or CSS Modules instead of runtime CSS-in-JS.
  • Keep as much of your page on the server as possible.
  • Use the RSC Parser to find hidden bloat in your data.

Was this helpful?

supported.

Read related documentation

No related documentation available.

Explore more guides

No related guides available.