Open ten random Next.js App Router projects on GitHub right now. I'd bet real money that at least seven of them have a 'use client' directive sitting at the top of a page component that never needed to be there in the first place, quietly shipping way more JavaScript to the browser than the page actually requires.
This is not a hypothetical problem. I've audited client projects where removing three unnecessary 'use client' directives cut the initial JS bundle by more than a third. Nobody put them there maliciously. Everybody put them there out of habit, muscle memory from the Pages Router days, or because one small interactive piece "made it easier" to just mark the whole file client-side instead of extracting it.
Why This Happens
The App Router defaults to Server Components. No JavaScript for that component ships to the browser at all, it renders on the server and sends HTML. The moment you add 'use client', that component and everything it imports becomes part of the client bundle.
The trap is that 'use client' doesn't just affect the file it's written in. It affects every component that file renders, unless those child components are passed in as children from a Server Component parent. So a page-level 'use client' for one button with an onClick handler can silently drag the entire page's markup, every heading, every paragraph, every static section, into the client bundle with it.
What This Actually Looks Like in Real Code
// ❌ The whole page becomes client-side JavaScript
'use client';
export default function ProductPage({ product }) {
const [inCart, setInCart] = useState(false);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<img src={product.image} alt={product.name} />
<ReviewsSection reviews={product.reviews} />
<RelatedProducts productId={product.id} />
<button onClick={() => setInCart(true)}>Add to cart</button>
</div>
);
}
Every one of those components, the reviews section, the related products list, none of which need any client-side interactivity at all, now ships as JavaScript because of one button at the bottom of the file.
// ✅ Only the actual interactive piece ships as client JS
export default function ProductPage({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<img src={product.image} alt={product.name} />
<ReviewsSection reviews={product.reviews} />
<RelatedProducts productId={product.id} />
<AddToCartButton productId={product.id} />
</div>
);
}
// components/AddToCartButton.tsx
'use client';
export function AddToCartButton({ productId }) {
const [inCart, setInCart] = useState(false);
return <button onClick={() => setInCart(true)}>Add to cart</button>;
}
Same functionality. The page component itself never runs in the browser at all. ReviewsSection and RelatedProducts stay Server Components, rendered entirely on the server, shipping zero JavaScript for content that was never interactive to begin with.
The Check That Actually Catches This
Don't trust your gut on this. Run the bundle analyzer and look:
npm install @next/bundle-analyzer
ANALYZE=true npm run build
If a page you'd expect to be mostly static, a blog post, a product page, a marketing page, shows up with a suspiciously large client bundle, that is almost always a 'use client' boundary sitting higher in the tree than it needs to be. I've caught this exact issue on client projects more times than I can count, always the same root cause, an interactive piece pulling its entire static parent along with it.
The Rule I Actually Follow Now
Push 'use client' as far down the component tree as it can possibly go. Not "this page has one interactive thing, so I'll mark the page client." Extract that one interactive thing into its own small component, mark only that component client, and let everything else stay server-rendered.
A useful mental test before adding the directive: does this specific piece of code need useState, useEffect, an event handler, or a browser API. If the answer is no, it does not need 'use client', even if something nearby does.
Where People Push Back on This
I know the counterargument. "It's simpler to just mark the whole page client and not think about the boundary." That's true, and for a small personal project, it might genuinely not matter. For anything with real traffic, real users on real connections, that simplicity is costing your actual users load time and interactivity delay, every single visit, forever, in exchange for a few minutes saved once during development.
The other pushback I hear: "Doesn't this add complexity, splitting things into more files?" A little, yes. It's also exactly the kind of complexity that pays for itself immediately in bundle size, and it becomes second nature within a few projects, not a genuine ongoing tax on how you write code.
Go check your own bundle right now. I'd bet you find at least one page where a 'use client' boundary is sitting higher in the tree than it actually needs to be. Drop what you find in the comments, curious how common this actually is across real projects versus how common I think it is.
Get the templates: https://pixelanas.gumroad.com
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (0)