Keep 'use client' on the Smallest Possible Leaf Components
Impact: HIGH (reduces client-side JavaScript bundle size and improves initial page load)
Every component marked with 'use client' -- and its entire import tree -- ships JavaScript to the browser. Placing 'use client' on a page component, layout, or large wrapper forces the entire subtree to be client-rendered, eliminating the benefits of Server Components (zero JS, direct data access, streaming). Push 'use client' down to the smallest interactive leaf component.
The composition pattern lets you keep most of your component tree server-rendered while wrapping small interactive parts in client components.
Incorrect ('use client' on large components that mostly render static content):
// app/rules/page.tsx// ❌ Entire page is a client component — ships everything to the browser'use client'import { useState, useEffect } from 'react'import { RulesList } from '@/components/RulesList'import { RulesFilter } from '@/components/RulesFilter'import { RulesStats } from '@/components/RulesStats'export default function RulesPage() { const [rules, setRules] = useState([]) const [filter, setFilter] = useState('all') useEffect(() => { fetch('/api/rules').then(r => r.json()).then(setRules) }, []) // ❌ RulesList and RulesStats are pure display but forced client-side // because the parent is 'use client' return ( <div> <h1>Rules</h1> <RulesStats rules={rules} /> <RulesFilter value={filter} onChange={setFilter} /> <RulesList rules={rules} filter={filter} /> </div> )}
// components/RulesStats.tsx// ❌ This is pure display — doesn't need 'use client' but is forced into// the client bundle because it's imported by a client component'use client'export function RulesStats({ rules }: { rules: Rule[] }) { return ( <div> <span>Total: {rules.length}</span> <span>Active: {rules.filter(r => r.active).length}</span> </div> )}
Correct (server page with interactive leaves):
// app/rules/page.tsx// ✅ Server Component — no JS shipped, direct data accessimport { Suspense } from 'react'import { ruleService } from '@/lib/services'import { RulesStats } from '@/components/RulesStats'import { RulesListWithFilter } from '@/components/RulesListWithFilter'export default async function RulesPage() { const result = await ruleService.getRulesForCurrentUser() const rules = result.success ? result.data : [] return ( <div> <h1>Rules</h1> {/* ✅ Pure display — stays server-rendered, zero JS */} <RulesStats rules={rules} /> {/* ✅ Only the interactive filter is a client component */} <RulesListWithFilter initialRules={rules} /> </div> )}
// components/RulesStats.tsx// ✅ No 'use client' — pure Server Component, ships zero JavaScriptexport function RulesStats({ rules }: { rules: Rule[] }) { return ( <div> <span>Total: {rules.length}</span> <span>Active: {rules.filter(r => r.active).length}</span> </div> )}
The composition pattern (server parent passes children to client wrapper):
// components/CollapsibleSection.tsx// ✅ Client component provides interactivity — children stay server-rendered'use client'import { useState, type ReactNode } from 'react'export function CollapsibleSection({ title, children,}: { title: string children: ReactNode}) { const [open, setOpen] = useState(true) return ( <section> <button onClick={() => setOpen(!open)}>{title}</button> {open && children} </section> )}// app/rules/page.tsx — Server Component uses client wrapper// ✅ HeavyContent stays server-rendered even though CollapsibleSection is clientexport default async function RulesPage() { const data = await fetchHeavyData() return ( <CollapsibleSection title="Details"> <HeavyContent data={data} /> {/* Server Component — zero JS */} </CollapsibleSection> )}
Rule of thumb: If a component does not use useState, useEffect, useRef, event handlers, or browser APIs, it should not have 'use client'.
Detection hints:
# Find 'use client' in page files (likely too high in the tree)grep -rn "use client" src/app --include="*.tsx" -l# Find large client componentsgrep -rn "use client" src/components --include="*.tsx" -l