Next.js path · Shipping it · Topic 11 of 11

Common Next.js performance pitfalls

Not started

Next.js gives you prerendering, streaming and several caches, but each one can be defeated by an innocent-looking line. The usual suspects are data waterfalls in Server Components, request data read too high in the tree, client boundaries placed on whole pages, oversized props, and caches that are configured but never hit. Each has a visible symptom (a ƒ in the build table, a large RSC Payload, a slow Time to First Byte) and a small structural fix.

Why it matters: Most slow Next.js apps are not slow because of React. They wait for data one call after another, ship components that didn't need to be client-side, or turn a whole route dynamic because a layout read a cookie.

  1. 1Start work in parallel

    Sequential `await`s in Server Components form a waterfall. Start independent requests together, or pass promises down and let Suspense boundaries wait separately.

  2. 2Keep request data low

    A layout that awaits `cookies()` or `params` at the top drags everything below it out of the static shell. Read request data in the smallest component that needs it.

  3. 3Watch what crosses the boundary

    Props to client components are serialised into the RSC Payload. Pass the three fields the UI shows, not the whole database row.

  4. 4Check the cache really hits

    In-memory caches on serverless instances, `revalidatePath("/", "layout")` after every change, and very short `cacheLife` profiles all look like caching but rarely serve from cache.

1

How it works

The explanation, with examples you can run.

Waterfalls

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

In a Server Component the fix is the same: start independent work before awaiting it. Better still, give each slow part its own component and Suspense boundary, so the page streams each part as it arrives instead of waiting for the slowest. When several components need the same data, wrap the loader in React's cache (one call per request) or use cache (shared across requests) rather than threading props through the tree.

Request data too high

// Slow: the whole layout waits for the request, nothing below it is in the shell.export default async function Layout({ children }: LayoutProps<"/shop">) {  const theme = (await cookies()).get("theme")?.value  return <div data-theme={theme}><Nav />{children}</div>}

Move the cookie read into a small component wrapped in <Suspense>, or into a client component that reads it in the browser, and Nav and the pages return to the static shell. The same goes for params: pass the promise down and await it where it is used.

Too much on the client

A "use client" page imports its whole subtree into the bundle. Put the directive on interactive leaves, pass server content as children, and check for large libraries imported from client modules (date libraries, Markdown, charting). Props are serialised into the RSC Payload; sending big objects inflates every navigation and can leak fields.

Caches that don't cache

  • Runtime use cache entries live in instance memory. On serverless they often miss; use use cache: remote for hot, expensive data.
  • revalidatePath("/", "layout") after every mutation invalidates the whole site. Tag data narrowly and invalidate the tag.
  • A seconds profile or revalidate: 0 is excluded from the shell and becomes a hole; make sure that is what you want.

Elsewhere

Keep Proxy fast and narrowly matched, because it runs before every matched request. Don't use Server Actions to read data; they run one at a time. Give the LCP image preload and correct sizes, and let next/font handle web fonts.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A page awaits `getUser()`, then `getOrders()`, then `getRecommendations()`, each taking about 200 ms and independent of the others. Roughly how long until it can render?
  2. 2.Which change usually moves more of a page into the static shell?
  3. 3.A Server Component passes a full `user` row (40 columns, including a password hash) to a client avatar. What are the two problems?

0 of 3 right so far. Get all three to finish this topic.