I wrote earlier about a buried cookies() call silently forcing an entire page into dynamic rendering. searchParams causes the exact same category of behavior, through a completely different, and in some ways more surprising, mechanism, since using it doesn't look like reading anything server-specific at all.
Why This One Is Extra Surprising
cookies() and headers() at least sound like they're reaching into something request-specific, so forcing dynamic rendering feels intuitive once you think about it. searchParams just looks like reading a prop, a completely normal-looking destructure at the top of a page component. There's nothing about the syntax that signals "this is a server-specific value tied to this exact request."
// app/products/page.tsx
export default function ProductsPage({
searchParams,
}: {
searchParams: Promise<{ category?: string; page?: string }>;
}) {
// reading searchParams here means this page can no longer be
// statically generated, full stop, regardless of what else it does
}
The moment a page component accepts and uses searchParams, Next.js treats the entire page as dynamic. This makes complete sense once you think through why, the query string is part of the actual request URL, and a page reading it necessarily produces different output for /products?category=shoes versus /products?category=hats, which is inherently incompatible with generating one fixed, static HTML output ahead of time. It's correct, intentional behavior. It's just not obviously signaled by the code itself the way reading cookies() is.
Where This Actually Costs You
For a genuinely filterable page, a product listing with category and sort filters, this dynamic behavior is exactly correct and necessary, the whole feature depends on rendering different content per query string. This isn't a bug to fix in that case.
The actual cost shows up when searchParams gets accepted by a page component out of habit, or copied from a similar page, without the page actually using the specific query params in any way that changes its output.
// app/about/page.tsx
export default function AboutPage({
searchParams, // accepted but never actually used for anything meaningful
}: {
searchParams: Promise<{ [key: string]: string | undefined }>;
}) {
return (
<div>
<h1>About Us</h1>
<p>We build things.</p>
</div>
);
}
If this component's type signature includes searchParams but the actual rendered output never varies based on it, simply accepting the prop can still be enough to affect how Next.js treats the route, depending on whether the value is actually read and used within the render. A genuinely static about page, forced into unnecessary dynamic rendering because of a leftover parameter or copy-pasted type signature from a different page, loses real caching benefit for no actual functional reason.
How to Tell If This Is Actually Happening
Same check as the earlier cookies() post, run a real build and look at the route summary:
npm run build
A route marked dynamic (ƒ) that you'd expect to be static (○) is worth investigating. If the page genuinely needs to vary based on query parameters, that's correct and expected. If it doesn't, actually use those params to produce different output, the fix is simply not accepting or reading searchParams in that component at all.
The Distinction That Actually Matters
Does this specific page's rendered output genuinely need to differ based on the query string? If yes, dynamic rendering triggered by searchParams is correct, intentional, and not something to fight against, the feature depends on it.
Is searchParams present in the component's signature or destructured, but never actually used to change what gets rendered? That's the case worth fixing, either by removing the unused parameter entirely, or, if the params are genuinely needed for something that doesn't affect the main rendered content, handling that specific piece in a way that doesn't force the whole page dynamic, a client-side read via useSearchParams() inside a small Client Component, for something that only needs to react to params after the initial static render.
// A pattern that keeps the main page static while still reacting to query params
// for a small, specific piece of UI that genuinely needs them
export default function ProductsPage() {
return (
<div>
<h1>Our Products</h1>
<ProductGrid /> {/* genuinely static content */}
<QueryAwareBanner /> {/* small client component reading useSearchParams() */}
</div>
);
}
The Broader Pattern
This is the same underlying lesson as the cookies() post, applied to a different, less obviously "server-specific" mechanism. Next.js's static-versus-dynamic decision is driven by what a page actually reads and depends on, not by how obviously server-related that code looks at a glance. searchParams, cookies(), and headers() all trigger the same category of behavior, and all three are easy to include without actually needing what they provide, especially when copying a page's structure from a similar one that genuinely did need it.
Go check your own build output for any page accepting searchParams that doesn't actually use it to change what gets rendered. If you find one, that's a free, easy static-rendering win sitting right there. 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)