Next.js Discord

Discord Forum

Next.js 15.3.2 - Keep SSR for server DOM wrapped by client/Suspense (without force-dynamic)?

Unanswered
Black Catbird posted this in #help-forum
Open in Discord
Black CatbirdOP
In dev, my server-rendered DOM (e.g. headings) appears in initial HTML. In prod (next build && next start), those elements disappear from page source when they’re rendered inside a Client wrapper and/or a Suspense boundary - unless I set dynamic='force-dynamic', which I don’t want (kills Full Route Cache).

Not all routes use searchParams, so this isn’t only about query strings. It’s about server DOM nested under client and/or Suspense.

E.g.
// page.tsx (Server)
export const revalidate = 86400; // keep caching
export default async function Page() {
  const data = await getData();           // resolves at build / ISR
  const title = data.title;               // SEO-critical

  return (
    <>
      {/* This shows in dev, but not in prod when nested below */}
      <ClientShell>                      {/* "use client" */}
        <Suspense fallback={<Skeleton/>}>
          <ServerSection>                {/* Server Component */}
            <h1>{title}</h1>             {/* disappears from initial HTML in prod */}
          </ServerSection>
        </Suspense>
      </ClientShell>
    </>
  );
}


What’s the recommended pattern to preserve SSR of SEO-critical DOM (keep it in initial HTML + maintain cache HIT with dynamic='auto') while still using client providers and/or Suspense deeper in the tree?
Is the guidance to render SSR-critical DOM before any client/Suspense boundary, and defer the rest under Suspense (PPR-style)? Any other caveats or segment-splitting patterns to avoid this prod-only behavior?

Env: Next.js 15.3.2, App Router, dynamic='auto', route-level revalidate in place.

0 Replies