在 Next.js 中,客户端组件和服务端组件是两种不同的渲染环境,决定了代码在哪里执行(服务器 vs 浏览器),直接影响 SEO、性能和安全性。
📊 核心区别
| 维度 | 服务端组件 (Server Component) | 客户端组件 (Client Component) |
|---|---|---|
| 执行环境 | Node.js 服务器 | 用户浏览器 |
| 包大小 | 零 | 需要打包发送到浏览器 |
| SEO | ✅ 完美支持 | ❌ 需要额外处理 |
| 访问浏览器 API | ❌ 不可能 | ✅ 可以 (window, document) |
| 访问后端 API | ✅ 直接(服务器间通信) | ❌ 需要跨域 |
| 交互性 | ❌ 无 | ✅ 事件监听、状态、效果 |
🎯 场景示例
场景1:NFT 市场首页
需求:展示热门 NFT 列表,需要 SEO,无需交互
tsx
// app/page.tsx - 服务端组件(默认)
// 这是服务端组件,因为没有 'use client'
import { db } from '@/lib/db'
import { fetchNFTsFromIPFS } from '@/lib/ipfs'
export default async function HomePage() {
// 1. ✅ 在服务器上直接读取数据库
const featuredNFTs = await db.nft.findMany({
where: { featured: true },
take: 10
})
// 2. ✅ 在服务器上获取 IPFS 数据
const nftsWithMetadata = await Promise.all(
featuredNFTs.map(async (nft) => {
const metadata = await fetchNFTsFromIPFS(nft.ipfsHash)
return { ...nft, metadata }
})
)
// 3. ✅ 安全的 API 调用(不暴露密钥)
const floorPrice = await fetch(
'https://api.opensea.io/api/v1/stats',
{ headers: { 'X-API-KEY': process.env.OPENSEA_API_KEY! } }
).then(res => res.json())
return (
<div>
<h1>热门 NFT</h1>
<p>市场地板价: {floorPrice.eth_price} ETH</p>
{/* 服务端组件内包含客户端组件 */}
<NFTGallery nfts={nftsWithMetadata} />
{/* 统计信息 - 完全静态 */}
<Stats
totalVolume={floorPrice.total_volume}
totalSales={floorPrice.total_sales}
/>
</div>
)
}
场景2:NFT 画廊组件
需求:需要交互(点击收藏、悬停效果)
tsx
// components/NFTGallery.tsx
'use client' // 必须声明
import { useState } from 'react'
import { Heart, ShoppingCart } from 'lucide-react'
import { useAccount, useWriteContract } from 'wagmi'
type NFT = {
id: number
name: string
image: string
price: string
}
export function NFTGallery({ nfts }: { nfts: NFT[] }) {
const { address, isConnected } = useAccount()
const [favorites, setFavorites] = useState<number[]>([])
const { writeContract, isPending } = useWriteContract()
// 1. ✅ 客户端状态
const handleFavorite = (nftId: number) => {
setFavorites(prev =>
prev.includes(nftId)
? prev.filter(id => id !== nftId)
: [...prev, nftId]
)
}
// 2. ✅ 客户端交互
const handleBuy = async (nft: NFT) => {
if (!address) {
// 使用浏览器 API
alert('请先连接钱包')
return
}
writeContract({
address: '0x...',
abi: [...],
functionName: 'buyNFT',
args: [nft.id],
value: parseEther(nft.price)
})
}
return (
<div className="grid grid-cols-3 gap-4">
{nfts.map((nft) => (
<div
key={nft.id}
className="group relative border rounded-lg p-4 hover:shadow-lg transition-shadow"
// 3. ✅ 客户端事件
onMouseEnter={() => console.log('hover')}
>
<img src={nft.image} alt={nft.name} />
<h3>{nft.name}</h3>
<p>{nft.price} ETH</p>
{/* 4. ✅ 客户端交互按钮 */}
<div className="flex gap-2 mt-2">
<button
onClick={() => handleFavorite(nft.id)}
className="p-2 hover:bg-gray-100 rounded"
>
<Heart className={`w-5 h-5 ${
favorites.includes(nft.id) ? 'fill-red-500 text-red-500' : ''
}`} />
</button>
<button
onClick={() => handleBuy(nft)}
disabled={!isConnected || isPending}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
{isPending ? '购买中...' : '购买'}
</button>
</div>
</div>
))}
</div>
)
}
场景3:混合使用示例
需求:一个页面包含静态内容 + 动态交互
tsx
// app/product/[id]/page.tsx
// 这个页面是服务端组件(默认)
import { db } from '@/lib/db'
import { notFound } from 'next/navigation'
import ProductImage from '@/components/ProductImage' // 客户端
import AddToCart from '@/components/AddToCart' // 客户端
import ProductReviews from '@/components/ProductReviews' // 服务端
export default async function ProductPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// 1. ✅ 服务端:从数据库获取数据
const product = await db.product.findUnique({
where: { id: parseInt(id) }
})
if (!product) {
notFound()
}
// 2. ✅ 服务端:安全获取外部 API
const relatedProducts = await fetch(
`${process.env.INTERNAL_API_URL}/recommendations/${id}`,
{ headers: { 'API-Key': process.env.INTERNAL_API_KEY! } }
).then(res => res.json())
return (
<div className="max-w-6xl mx-auto p-4">
{/* 3. ✅ 静态内容 - 服务端渲染 */}
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-gray-600 mt-2">{product.description}</p>
{/* 4. ✅ 客户端组件:交互式图片 */}
<div className="grid grid-cols-2 gap-8 mt-8">
<ProductImage
images={product.images}
productId={product.id}
/>
{/* 5. ✅ 客户端组件:购物车交互 */}
<div>
<p className="text-2xl font-semibold">${product.price}</p>
<AddToCart productId={product.id} />
{/* 6. ✅ 服务端组件:评论列表 */}
<ProductReviews productId={product.id} />
</div>
</div>
{/* 7. ✅ 静态相关内容 - 服务端渲染 */}
<div className="mt-12">
<h2 className="text-2xl font-semibold mb-4">相关商品</h2>
<div className="grid grid-cols-4 gap-4">
{relatedProducts.map((related) => (
<a
key={related.id}
href={`/product/${related.id}`}
className="border rounded-lg p-4 hover:shadow-md"
>
<h3 className="font-medium">{related.name}</h3>
<p className="text-sm text-gray-500">${related.price}</p>
</a>
))}
</div>
</div>
</div>
)
}
🎯 决策指南:什么时候用什么?
用服务端组件:
tsx
// ✅ 读取数据库/API
// ✅ 访问安全环境变量
// ✅ 大型依赖(避免打包到客户端)
// ✅ 提高性能(减少客户端JS)
// ✅ 更好的SEO
用客户端组件:
tsx
'use client'
// ✅ 用户交互(onClick, onChange)
// ✅ 状态和生命周期(useState, useEffect)
// ✅ 浏览器API(window, document, localStorage)
// ✅ 第三方库(需要DOM的图表、地图)
// ✅ Web3集成(wagmi, 钱包连接)
🔧 最佳实践模式
模式1:服务端获取数据,客户端交互
tsx
// 服务端组件
export default async function Dashboard() {
const data = await getData() // 服务端获取
return (
<div>
<StaticContent data={data} />
<InteractiveChart data={data} /> {/* 客户端组件 */}
</div>
)
}
模式2:客户端组件中获取数据
tsx
'use client'
export function InteractiveChart({ initialData }) {
const [data, setData] = useState(initialData)
useEffect(() => {
// 客户端获取实时数据
fetch('/api/realtime-data')
.then(res => res.json())
.then(setData)
}, [])
return <Chart data={data} />
}
模式3:动态导入大型客户端组件
tsx
import dynamic from 'next/dynamic'
// 避免重型组件影响首屏加载
const HeavyChart = dynamic(
() => import('@/components/HeavyChart'),
{
ssr: false, // 不在服务端渲染
loading: () => <Spinner />
}
)
⚠️ 常见错误
tsx
// ❌ 错误:在服务端组件中使用客户端特性
export default async function Page() {
const [state, setState] = useState(0) // 错误!
useEffect(() => {}) // 错误!
return <div onClick={() => {}}>点击</div> // 错误!
}
// ✅ 正确:提取为客户端组件
'use client'
export function InteractivePart() {
const [state, setState] = useState(0)
return <div onClick={() => setState(state + 1)}>{state}</div>
}
💡 总结
- 默认用服务端组件:更好的性能、SEO、安全性
- 需要交互时用客户端组件 :加
'use client' - 混合使用:页面的静态部分用服务端,交互部分用客户端
- 数据流:服务端获取初始数据 → 传递给客户端组件
一句话决策:
问自己:用户需要和它交互吗?
是 → 客户端组件
否 → 服务端组件