DEV Community

Anas Sheikh
Anas Sheikh

Posted on

One Buried cookies() Call Can Silently Kill Static Rendering for Your Entire Page

Here's a question worth actually checking on your own app: is that marketing page you assume is static actually being pre-rendered, or is something buried deep in its component tree quietly forcing it to render fresh on every single request?

Calling cookies() or headers() anywhere in a page's render tree opts that entire route out of static rendering. Not just the specific component that called it, the whole page. This is documented behavior, not a bug, but it catches people constantly because the call causing it is often nowhere near the part of the page anyone would suspect.

A Realistic Version of This

// app/page.tsx (your marketing homepage, should be static)
export default function HomePage() {
  return (
    <div>
      <Hero />
      <Features />
      <Pricing />
      <ThemeAwareFooter /> {/* looks innocent */}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// components/ThemeAwareFooter.tsx
import { cookies } from 'next/headers';

export default async function ThemeAwareFooter() {
  const cookieStore = await cookies();
  const theme = cookieStore.get('theme')?.value ?? 'dark';

  return <footer className={theme}>© 2026</footer>;
}
Enter fullscreen mode Exit fullscreen mode

A single cookies() call, buried inside a footer component nobody would think twice about, is enough to force the entire homepage into dynamic rendering. That marketing page you assumed was being generated once and served instantly from a CDN edge cache is instead being rendered fresh, on your server, for every single visitor, all because of a theme preference in a footer.

Why This Is So Easy to Miss

Locally, in development, everything renders dynamically anyway, so there's no visible difference between a properly static page and one accidentally forced dynamic by a buried cookies() call. The performance gap only shows up in production, under real caching behavior, and even then it often doesn't throw an obvious signal. The page still works. It's just quietly slower and more expensive to serve than it should be, for reasons that don't show up anywhere in an error log.

How to Actually Check

npm run build
Enter fullscreen mode Exit fullscreen mode

The build output tells you directly. Next.js marks each route with a symbol indicating whether it was statically generated or will render dynamically on each request. A route you expected to see marked static (○) showing up as dynamic (λ or ƒ depending on version) is exactly the signal that something in that page's tree is opting it out, and it's worth tracing down which component is actually responsible.

The Actual Fix

The component causing the problem usually doesn't need to read the cookie server-side at all. A theme preference is a great example, it can be read client-side instead, after the static HTML has already been served.

// components/ThemeAwareFooter.tsx
'use client';
import { useEffect, useState } from 'react';

export default function ThemeAwareFooter() {
  const [theme, setTheme] = useState('dark'); // safe default for the static shell

  useEffect(() => {
    const stored = document.cookie
      .split('; ')
      .find((row) => row.startsWith('theme='))
      ?.split('=')[1];
    if (stored) setTheme(stored);
  }, []);

  return <footer className={theme}>© 2026</footer>;
}
Enter fullscreen mode Exit fullscreen mode

This keeps the page itself statically generated, served instantly from cache, while the theme preference gets applied client-side after hydration. There's a brief moment where the default theme shows before the real preference is applied, which is a reasonable tradeoff for most cases, and for anything where that flash genuinely matters, a small inline script setting the class before hydration is a more involved but still static-compatible fix.

When Reading Cookies Server-Side Is Actually Necessary

Not every case should be pushed to the client. Genuine per-user, security-relevant data, an actual auth session, a real feature flag tied to account state, legitimately needs a server-side cookies() read, and in those cases, the page being dynamic is correct, not a mistake to fix. The point isn't "never call cookies() on the server," it's noticing when a call that doesn't actually need to be there is quietly costing an entire otherwise-static page its caching benefits.

The Rule Worth Remembering

If a piece of data genuinely varies per authenticated user or per security context, dynamic rendering is the right, intentional choice, keep it server-side. If it's a preference that could just as easily be applied after the page loads, theme, a dismissed banner, a UI setting with no security implication, moving it to the client preserves static rendering for everything else on that page.


Go run npm run build on your own project right now and check whether any page you assumed was static is actually marked dynamic. If you find one, trace down which component is responsible, I'd genuinely be curious how often the culprit turns out to be something as unassuming as a footer or a theme toggle. Drop what you find in the comments.

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)