new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Next.js · Guides · all subjects

building/prerendering

114 notes in this subject, read out of this brain and free to use. This is page 1 of 2.

Pages reading cookies, headers, or searchParams stay partially prerendered

Pages that read cookies(), headers(), or searchParams stay ◐ Partial Prerender, streaming those parts into the shell on each visit.

Building with prerendering example

Example showing a product page that reads uncached data by id. During build it fails with blocking-prerender-dynamic because params and uncached fetch run outside <Suspense>. Three fixes are shown: (1) Add loading.js for streaming with ◐ output. (2) Export generateStaticParams and wrap fetch in 'use cache' for full ○ prerendering of known params. (3) Export instant = false to allow blocking without fix. Each approach changes the build output symbol and user experience.

Cached data vs uncached data in prerendered params

When generateStaticParams lists params without caching the data lookup, pages show ◐ because params are known but content comes from uncached I/O that only runs at request time. When the lookup uses 'use cache', pages show ○ because both params and data are available at build time, allowing full prerendering. Pages showing ◐ serve the static shell and stream content on each request.

PPR build-time artifacts

For each Partial Prerendering (PPR) route, Next.js produces three artifacts at build time: a static HTML shell containing all prerenderable content with Suspense fallbacks for dynamic areas, a postponedState value (a serialized string that must be treated as opaque and never parsed or modified), and an RSC payload for the static portions.

PPR shell and postponedState must be atomic

The static HTML shell and postponedState blob for a PPR route must be stored and updated atomically together. When a PPR route is revalidated via time-based or on-demand revalidation, Next.js regenerates both together. Serving a new shell with an old postponedState, or vice versa, produces incorrect dynamic content.

PPR adapter cache observation with onCacheEntryV2

Use requestMeta.onCacheEntryV2 in your adapter to observe cache updates and propagate them to your storage backend when PPR shells and postponed states are regenerated.

Origin-only PPR implementation

The simplest PPR approach is origin-only: all requests go directly to the Next.js server, which reads the shell from its local cache, sends it immediately, then renders and streams dynamic content. This requires only that the platform supports streaming HTTP responses. This is what next start does by default.

CDN shell with origin compute for PPR

For better TTFB with PPR, the static HTML shell can be cached at the CDN edge. When a request arrives, the CDN serves the cached shell immediately (edge latency), sends a resume request to the origin server ideally in parallel with streaming the shell, and the origin server renders only dynamic portions and streams them back. The CDN concatenates the shell and dynamic content into a single streaming response to the client. This requires the CDN to support combining cached and dynamic content in a single streaming response.

PPR shell edge latency optimization

For lowest possible latency, the PPR shell can be served from edge storage (for example, a KV store populated during onBuildComplete) rather than from a CDN cache. This is a platform architecture decision and does not require changes to the Next.js application.

PPR resume protocol header

The resume protocol tells the Next.js handler to skip the shell and render only dynamic portions. In CDN-to-origin architectures, send a POST request to the route with the header 'next-resume: 1' and include the postponedState blob as the request body. The server will render only deferred Suspense boundaries and stream the result.

PPR resume protocol with Server Actions

When a POST request combines a Server Action with a PPR resume, the request body contains the postponed state followed by the action body. The 'x-next-resume-state-length' header carries the byte length of the postponed state prefix so the handler can separate the two. For a pure PPR resume, the entire request body is the postponed state and this header is not needed.

PPR adapter-based resume protocol

In adapter-based platforms, call the entrypoint handler with req.method set to 'POST', the 'next-resume: 1' header on the request, and the postponedState as the request body. Alternatively, pass requestMeta: { postponed: postponedState } as the third argument to the handler invocation, which bypasses the HTTP layer entirely. The handler renders only deferred Suspense boundaries and streams the result to res without needing an HTTP round-trip.

PPR implementation checklist

PPR implementation requires five steps: (1) Read PPR outputs at build time by identifying prerenders with renderingMode 'PARTIALLY_STATIC' in the adapter's onBuildComplete, and store the shell HTML and postponedState in your cache. (2) Serve the shell at request time for incoming PPR route requests by serving the cached shell immediately and beginning to stream. (3) Resume dynamic rendering by sending a POST request with 'next-resume: 1' header and postponed state body for CDN-to-origin, or calling the handler directly with POST method for adapter-based platforms. (4) Handle cache updates using requestMeta.onCacheEntryV2 to capture new shell and postponed state pairs after revalidation and update your cache atomically. (5) Support graceful degradation by falling back to a full server render if postponed state is unavailable or stale, so the user gets a complete page without the shell-first optimization.

Finding PPR routes in adapter output

In the adapter output, PPR routes are identified by 'renderingMode: "PARTIALLY_STATIC"' in the prerenders array. Iterate outputs.prerenders to find these entries and read fallback.postponedState. pprChain.headers contains the headers needed for the resume protocol: { 'next-resume': '1' }.

PPR postponedState opacity requirement

The postponedState value produced at build time must be treated as opaque: it must be passed through without parsing or modifying it. Altering postponedState produces incorrect dynamic rendering output.

PPR request-time flow

When a request arrives for a PPR route, the server sends the static HTML shell to the client immediately, then resumes rendering dynamic portions using the postponed state, and streams dynamic content to the client so React can hydrate deferred Suspense boundaries. The client sees the static shell instantly, then dynamic content appears as it resolves.

Generate sitemaps and robots files for search engines

Sitemaps and robots files should be generated to help search engines crawl and index pages in a Next.js application.

Global Error UI with app/global-error.tsx

app/global-error.tsx should be added to provide consistent, accessible fallback UI and recovery for uncaught errors across the application.

Global 404 with app/global-not-found.tsx

app/global-not-found.tsx should be added to serve an accessible 404 for unmatched routes across the application.

Server Components enabled by default in Next.js

Next.js uses Server Components by default. Server Components run on the server and do not require JavaScript to render on the client, so they have no impact on client-side JavaScript bundle size. Client Components can be used as needed for interactivity.

Prefetching links automatically

When a link to a new route enters the user's viewport, Next.js prefetches the route in the background, making navigation to new routes almost instant. Prefetching can be opted out where appropriate.

Prerendering in Next.js caches rendered results

Next.js prerenders Server and Client Components on the server at build time and caches the rendered result to improve application performance. Dynamic Rendering can be opted into for specific routes where appropriate.

Use layouts for partial rendering on navigation

Layouts should be used to share UI across pages and enable partial rendering on navigation in Next.js applications.

Request-time APIs opt routes into Dynamic Rendering

Request-time APIs like cookies() and the searchParams prop will opt the entire route into Dynamic Rendering, or the whole application if used in the Root Layout. Request-time API usage should be intentional and wrapped in Suspense boundaries where appropriate.

Use Metadata API for SEO optimization

The Metadata API should be used to improve a Next.js application's Search Engine Optimization (SEO) by adding page titles, descriptions, and more.

Create custom error pages for error handling

Custom error pages should be created to gracefully handle catch-all errors and 404 errors in production.

Resolving blocking behavior strategy

When a component's data blocks the response, there are two options to unblock it: cache the component so it becomes stable and can be prerendered with the rest of the page, or stream the component with Suspense so it becomes non-blocking and the rest of the page doesn't have to wait for it.

Static components definition

A static component is one that doesn't depend on inputs that change between requests, such as external data, request headers, route params, the current time, or random values. Static components have output that never changes and can be determined ahead of time, so Next.js can safely prerender the page at build time.

Dynamic components definition

A dynamic component depends on external data that can change over time, making the rendered output no longer guaranteed to be stable. When a dynamic component's data is fetched at request time without caching, the framework assumes you want fresh data on every user request, which can delay the entire route from responding.

Partial prerendering pattern

Partial prerendering (PPR) separates prerenderable work from request-time work by using Suspense boundaries. At build time, static and cached content is prerendered and pushed to a CDN. At request time, the prerendered part is served instantly from a CDN node, while dynamic content is rendered on the server and streamed to the client. This allows pages to include pockets of dynamic content without blocking the response.

Blocking prerender warning

When uncached data is awaited outside of a Suspense boundary in a component, Next.js shows a warning that accessing uncached data prevents the route from being prerendered. This warning protects developers from performance cliffs by indicating they need to either cache the component or stream it with Suspense.

Build time vs request time in partial prerendering

At build time, most of the page including static components, cached components, and Suspense fallbacks are rendered and pushed to a CDN. At request time, the prerendered part is served instantly from a CDN node close to the user, while dynamic content is rendered on the server and streamed to the client.

Public pages use case

Public pages show the same content to every user and can be prerendered ahead of time and reused. Common examples include landing pages, marketing pages, and product pages. Since data is shared across users, prerendering leads to faster page loads and lower server costs.

Example: static header component

```tsx function Header() { return <h1>Shop</h1> } ``` A static component that has no dependencies on changing inputs and can be prerendered at build time.

TZ and LANG environment variables catch locale mismatches in development

Run the dev server with `TZ` and `LANG` set differently from the browser to catch locale mismatches early. For example: `TZ=UTC LANG=ja_JP.UTF-8 next dev`. Use `TZ=UTC` as a good default since most servers run in UTC. This helps catch issues before they reach production.

When to use different approaches for client-specific formatting

Use `headers()` or `cookies()` to read request data when date depends on request data (cookies, headers), and format server-side. Use a Client Component with `useEffect` and `suppressHydrationWarning` when date updates live (countdown timers, clocks). Format the date server-side using the `Accept-Language` header when the page is already fully dynamic. Use internationalization with per-locale static builds or dynamic rendering when translating content between languages.

Reading cookies in root layout opts app out of static prerendering

Reading cookies with `await cookies()` in the root layout opts the entire app out of static prerendering. Under Cache Components, it forces blocking every segment under the layout. To keep the page statically prerendered with a generic default and avoid flash, read the cookie in the inline script instead, which runs on the client.

Scripts inserted via DOM updates do not execute on client-side navigation

On client-side navigations via `<Link>`, scripts inserted via DOM updates don't execute in the browser. React renders the component from the RSC payload, and the script won't run. Making the component a Client Component solves this: the formatting function runs directly in the browser on soft navigations, while the inline script (set to `type="text/plain"`) handles hard navigations.

Content Security Policy blocks inline scripts without nonce

Inline scripts with `dangerouslySetInnerHTML` are blocked by strict Content Security Policies that do not allow `'unsafe-inline'`. If an app uses a CSP, a nonce must be added to the script tag to allow the inline script to execute.

Hydration mismatch causes visible flash on screen

When a Client Component renders differently on the server than on the client (e.g., due to locale differences), React detects the mismatch during hydration, throws a hydration error, and the user sees a visible flash as the DOM is corrected. For example, `toLocaleDateString()` formats dates differently on the server (using server locale) versus in the browser (using client locale), causing SSR to produce '6/15/2026' while hydration produces '2026/6/15'.

suppressHydrationWarning tells React to accept DOM over payload

The `suppressHydrationWarning` prop on an element tells React to accept whatever value is currently in the DOM and discard the client's output for that element during hydration. Without it, React treats a text mismatch as a hydration error and recovers by client-rendering from the nearest error or Suspense boundary, causing a flash and losing inline script corrections on other components within that boundary because scripts don't re-execute when React rebuilds the DOM.

Inline scripts execute synchronously before first paint

Inline scripts placed via `dangerouslySetInnerHTML` in the HTML run synchronously during browser HTML parsing, before the first paint and before React loads. This timing allows them to update the DOM with correct client-specific values (like locale-formatted dates or theme preferences from localStorage) before the user sees any content, eliminating the flash.

useEffect causes visible flash because it runs after paint

`useEffect` runs after hydration and paint are complete. If used to update client-specific values, the user sees the server-rendered value first, then the correction appears as a flash. `useLayoutEffect` runs before paint but after hydration, which prevents the flash between hydration and paint but not the flash between HTML arriving and React hydrating. Inline scripts avoid both flashes because they run during HTML parsing, before React is involved.

Inline script for locale-formatted dates example

```tsx import { getEvent } from '@/app/lib/events' export default async function Page() { const event = await getEvent('nextjs-conf') return ( <section> <h1>{event.name}</h1> <p id="event-date" suppressHydrationWarning> {new Date(event.date).toLocaleDateString()} </p> <script dangerouslySetInnerHTML={{ __html: `document.getElementById("event-date").textContent=new Date("${event.date}").toLocaleDateString()`, }} /> </section> ) } ``` This example shows how to prevent flash for locale-specific date formatting: the server renders with its locale, then an inline script synchronously updates the text to the user's locale before first paint.

InlineScript helper component hides script type mismatch

```tsx export function InlineScript({ html }: { html: string }) { return ( <script type={typeof window === 'undefined' ? 'text/javascript' : 'text/plain'} suppressHydrationWarning dangerouslySetInnerHTML={{ __html: html }} /> ) } ``` React warns in development when rendering produces `<script>` tags. This helper sets `type="text/javascript"` on the server (so the script executes during parsing) and `type="text/plain"` on the client (so it does not execute), with `suppressHydrationWarning` to handle the type mismatch.

LocalDate client component with inline script for client-side navigation

```tsx 'use client' import { useId } from 'react' import { InlineScript } from './inline-script' export function LocalDate({ date, options, }: { date: string options?: Intl.DateTimeFormatOptions }) { const id = useId() return ( <> <time id={id} dateTime={date} suppressHydrationWarning> {new Date(date).toLocaleDateString(undefined, options)} </time> <InlineScript html={`{var n=document.getElementById("${id}");if(n)n.textContent=new Date("${date}").toLocaleDateString(undefined,${JSON.stringify(options)})}`} /> </> ) } ``` On hard navigation (initial load, refresh), the inline script executes and corrects the date before React hydrates. On client-side navigation via `<Link>`, `toLocaleDateString()` runs in the browser as part of the Client Component render, and the script is `type="text/plain"` and ignored. Use `<time>` element with `dateTime` attribute so search engines and screen readers can parse the ISO string.

Theme from localStorage inline script example

```tsx export default function RootLayout({ children }: LayoutProps<'/'>) { return ( <html lang="en" data-theme="light" suppressHydrationWarning> <head> <script dangerouslySetInnerHTML={{ __html: `(function(){try{var t=localStorage.getItem("theme");if(t)document.documentElement.setAttribute("data-theme",t)}catch(e){}})()`, }} /> </head> <body>{children}</body> </html> ) } ``` This inline script runs in `<head>` before any content is painted. It reads the theme from localStorage (with try/catch for unavailability) and sets a `data-theme` attribute on `<html>`. The correct theme is applied before the first paint.

Theme from cookie inline script example

```tsx export default function RootLayout({ children }: LayoutProps<'/'>) { return ( <html lang="en" data-theme="light" suppressHydrationWarning> <head> <script dangerouslySetInnerHTML={{ __html: `(function(){try{var m=document.cookie.match(/(?:^|; )theme=([^;]*)/);if(m)document.documentElement.setAttribute("data-theme",decodeURIComponent(m[1]))}catch(e){}})()`, }} /> </head> <body>{children}</body> </html> ) } ``` Inline script reads theme from the cookie instead of localStorage, allowing the server to send it via the cookie but keeping the page statically prerendered with a generic default. Reading cookies in the inline script instead of in the root layout avoids opting the entire app out of static prerendering.

Lazy useState initializer syncs state with inline script

```tsx const [openId, setOpenId] = useState(() => { if (typeof window === 'undefined') return DEFAULT_ID return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_ID }) ``` When a Client Component manages interactive state, use a lazy state initializer that reads from the same source as the inline script. Both the script and the initializer read from localStorage, so React's initial state always matches the DOM that the inline script set. This prevents hydration mismatches.

Accordion component with inline script for localStorage persistence

```tsx 'use client' import { useState, useCallback } from 'react' import { InlineScript } from './inline-script' const STORAGE_KEY = 'open-section' const sections = [ { id: 'setup', title: 'Setup', content: 'Install dependencies and create your project.' }, { id: 'usage', title: 'Usage', content: 'Import the component and pass your data.' }, { id: 'deploy', title: 'Deploy', content: 'Push to your Git provider and deploy.' }, ] const DEFAULT_ID = sections[0].id const SECTION_IDS = sections.map((s) => s.id) export function Accordion() { const [openId, setOpenId] = useState(() => { if (typeof window === 'undefined') return DEFAULT_ID return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_ID }) const handleToggle = useCallback( (id: string) => (e: React.ToggleEvent<HTMLDetailsElement>) => { if (e.newState === 'open') { setOpenId(id) localStorage.setItem(STORAGE_KEY, id) } }, [] ) return ( <div> {sections.map((section) => ( <details key={section.id} name="accordion" id={`section-${section.id}`} open={openId === section.id} onToggle={handleToggle(section.id)} > <summary>{section.title}</summary> <p>{section.content}</p> </details> ))} <InlineScript html={`{var id=localStorage.getItem("${STORAGE_KEY}")?"${DEFAULT_ID}";${JSON.stringify(SECTION_IDS)}.forEach(function(s){var el=document.getElementById("section-"+s);if(el){if(s===id)el.setAttribute("open","");else el.removeAttribute("open")}})}` } /> </div> ) } ``` This accordion persists which section is open to localStorage. The inline script sets the initial open state from localStorage before React hydrates, and the lazy `useState` initializer reads from the same source, ensuring React's state matches the DOM.

useLayoutEffect re-applies attribute after Strict Mode remount

```tsx 'use client' import { useLayoutEffect } from 'react' export function ThemeToggle() { useLayoutEffect(() => { const theme = localStorage.getItem('theme') if (theme) document.documentElement.setAttribute('data-theme', theme) }, []) function toggle() { const next = (localStorage.getItem('theme') ?? 'light') === 'dark' ? 'light' : 'dark' localStorage.setItem('theme', next) document.documentElement.setAttribute('data-theme', next) } return <button onClick={toggle}>Toggle theme</button> } ``` In development, React's Strict Mode remounts components and resets `<html>`, `<head>`, and `<body>` to only the attributes it manages from JSX, clearing attributes the inline script set. Using `useLayoutEffect` to re-apply the attribute is a no-op in production but fixes the attribute clearing on dev remount. `useLayoutEffect` runs before paint, so no flash occurs.

Setting theme to cookie for persistence

To switch themes and persist to a cookie: `const theme = 'dark'; document.documentElement.setAttribute('data-theme', theme); document.cookie = \`theme=${encodeURIComponent(theme)}; path=/; max-age=31536000; SameSite=Lax\`;`. The `max-age=31536000` sets expiration to one year.

Component-level boundaries approach trade-offs

Next.js uses component-level boundaries where static and dynamic content coexist within a single streaming response. A page can have a static shell that loads instantly, a cached function that revalidates independently, and a dynamic section that streams in as it resolves, all without the developer splitting anything into separate routes or client-side fetches. The trade-off is infrastructure complexity: a finer-grained rendering boundary transfers complexity from application code into the hosting platform.

Static and dynamic rendering boundary is at component level in Next.js

Next.js treats the boundary between static and dynamic rendering at the component level, not the route level. A single page can have a static shell that loads instantly and dynamic sections that stream in as they resolve. A cached function can live inside a dynamic route. A static page can be updated without a redeploy.

Partial Prerendering enables component-level static and dynamic spectrum

Partial Prerendering, Cache Components (use cache), and on-demand revalidation represent a rendering model that treats static and dynamic as a spectrum rather than a binary choice. They enable the boundary between static and dynamic to exist at the component level.

Benefits of component-level rendering boundary

The component-level rendering model provides three main benefits: (1) Faster perceived load times—the static shell renders immediately while dynamic content streams in, users see useful content right away. (2) Incremental caching—developers can add caching and revalidation incrementally without deciding upfront whether a route is static or dynamic; any page can be revalidated on demand and any function cached with use cache. (3) Granular caching—cache a function with use cache, not a route; revalidate a tag, not a deployment; an expensive database query can be cached independently of the rest of the page.

Build-time prerendering approach trade-offs

In build-time prerendering, every page is generated at build time, producing static files that can be served from any CDN or file server with zero runtime infrastructure. Dynamic content requires client-side fetching after the page loads. This is the simplest model to deploy, but every content change requires a rebuild and redeploy.

Route-level boundaries approach trade-offs

In route-level boundaries, each route chooses whether it is static or dynamic. Static routes are prerendered at build time, dynamic routes are server-rendered per request. The infrastructure splits cleanly: static files go to a CDN, dynamic routes go to a server. This is straightforward to reason about but the choice is all-or-nothing per route. A mostly-static page with one dynamic element (a user greeting, a live price) must either be fully dynamic or fetch that element on the client after load.

Infrastructure requirements for component-level rendering

The component-level rendering model requires four infrastructure capabilities: (1) Streaming is required because static and dynamic content are served in a single response; the server sends initial content first, then streams dynamic portions as they resolve. (2) Cache coordination is required when running multiple instances because any cached content can be invalidated on demand via revalidateTag() or revalidatePath(). (3) Cache consistency matters because revalidation regenerates both the HTML response and the RSC payload (the serialized React Server Components data used for client-side navigation); if these get out of sync, users may see inconsistent data during navigation. (4) PPR shell delivery at CDN latency can require additional platform integration to store the static shell separately and resume dynamic rendering correctly.

Functional fidelity vs performance fidelity in platform support

Functional fidelity means every Next.js feature works correctly on the platform; this is binary and determined by whether the adapter passes the adapter test suite. Performance fidelity means features achieve their optimal performance characteristics (e.g., PPR's static shell served at CDN latency rather than origin latency, or ISR serving stale content instantly while revalidating in the background); this is a spectrum and every platform will achieve different levels based on their architecture.

Give your agent this brain