性能调优实战 Checklist
性能不是功能做完后才考虑的事——它应该贯穿开发全过程。但现实中,大多数性能问题都是在上线后用户投诉才发现的。本章把 Next.js 性能优化整理成一份可执行的 Checklist,按优先级排序:先做投入产出比最高的优化,最后再做锦上添花的微调。
1. Bundle 优化
1.1 分析 Bundle 体积
首先要知道"大"在哪里:
# 方法 1:Next.js 内置分析
ANALYZE=true next build
# 方法 2:@next/bundle-analyzer
# next.config.ts
import withBundleAnalyzer from '@next/bundle-analyzer'
export default withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})({
// ... next config
})
# 运行
ANALYZE=true pnpm build
# 自动打开浏览器展示 treemap1.2 常见体积问题
| 问题 | 影响 | 修复 |
|---|---|---|
| Lodash 全量导入 | +70KB | import debounce from 'lodash/debounce' |
| Moment.js | +300KB | 换 date-fns(Tree-shakable) |
| icons 全量导入 | +100KB+ | import { Bell } from 'lucide-react' |
| 未 Tree-shake 的 UI 库 | 50-200KB | 用 shadcn/ui(只复制用到的组件) |
| 大型编辑器/图表库 | 200KB+ | dynamic(() => import(...), { ssr: false }) |
| polyfill 过多 | 50-100KB | 检查 browserslist,移除不需要的 |
1.3 Dynamic Import 策略
import dynamic from 'next/dynamic'
// 1. 大组件延迟加载
const RichEditor = dynamic(() => import('@/components/rich-editor'), {
loading: () => <Skeleton className="h-64" />,
ssr: false,
})
// 2. 条件加载(只在需要时加载)
const AdminPanel = dynamic(() => import('@/components/admin-panel'))
export default function Page({ isAdmin }: { isAdmin: boolean }) {
return (
<div>
<MainContent />
{isAdmin && <AdminPanel />} {/* 非 Admin 不加载 */}
</div>
)
}
// 3. 路由级延迟加载(Next.js 自动做了)
// app/ 下每个 page.tsx 自动 code split,无需手动处理1.4 第三方脚本优化
import Script from 'next/script'
// 分析脚本:afterInteractive(默认,页面可交互后加载)
<Script src="https://www.googletagmanager.com/gtag/js?id=G-XXX" />
// 非关键脚本:lazyOnload(浏览器空闲时加载)
<Script src="https://widget.intercom.io/widget/xxx" strategy="lazyOnload" />
// 关键脚本:beforeInteractive(在页面 hydrate 前加载)
<Script src="/critical-polyfill.js" strategy="beforeInteractive" />2. 缓存配置
2.1 四层缓存
Next.js 有四层缓存机制:
请求 → ① 客户端 Router Cache
→ ② 服务端 Full Route Cache
→ ③ 服务端 Data Cache(fetch/unstable_cache)
→ ④ React 内存缓存(Request Memoization)
2.2 fetch 缓存
// 不缓存(每次请求都执行)
const data = await fetch(url, { cache: 'no-store' })
// 按时间缓存(ISR)
const data = await fetch(url, { next: { revalidate: 3600 } }) // 1 小时
// 按标签缓存(手动失效)
const data = await fetch(url, { next: { tags: ['products'] } })
// 失效:revalidateTag('products')
// 静态缓存(构建时生成,不再更新)
const data = await fetch(url, { cache: 'force-cache' })2.3 数据库查询缓存
import { unstable_cache } from 'next/cache'
// 缓存数据库查询
const getProducts = unstable_cache(
async (tenantId: string) => {
return db.select().from(products).where(eq(products.tenantId, tenantId))
},
['products'],
{
revalidate: 300, // 5 分钟过期
tags: ['products'], // 支持手动失效
}
)
// 使用
const products = await getProducts(tenantId)
// 失效
revalidateTag('products')2.4 静态页面缓存
// 完全静态(构建时生成)
export default function PricingPage() {
return <div>...</div> // 无异步操作 → 自动静态
}
// ISR(定时重新生成)
export const revalidate = 3600 // 每小时重新生成
// 强制动态(每次请求都渲染)
export const dynamic = 'force-dynamic'2.5 缓存 Checklist
## 缓存配置 Checklist
### 页面级
- [ ] 营销页面(首页/定价/博客)→ 静态生成 + ISR(1小时)
- [ ] 文档页面 → 静态生成 + on-demand revalidation
- [ ] Dashboard → force-dynamic(实时数据)
- [ ] 设置页面 → force-dynamic
### 数据级
- [ ] 频繁读、不常变的数据 → unstable_cache(5-30分钟)
- [ ] 外部 API 调用 → fetch + revalidate
- [ ] 用户特定数据 → 不缓存
- [ ] 公共数据(产品列表、分类)→ 缓存 + tag 失效
### 失效策略
- [ ] 数据变更时调用 revalidatePath 或 revalidateTag
- [ ] revalidate 在 return 之前调用
- [ ] 测试缓存失效是否生效3. 渲染策略
3.1 选择渲染方式
页面类型 推荐渲染方式 理由
─────────────────────────────────────────────────
营销首页 SSG 内容不变,速度最快
博客文章 SSG + ISR 构建时生成,CMS 更新时重新生成
定价页面 SSG + ISR 偶尔更新价格
Dashboard SSR (dynamic) 实时数据,每次请求渲染
搜索结果页 SSR (dynamic) 依赖用户输入
用户设置 SSR (dynamic) 用户特定数据
API 文档 SSG 纯静态内容
3.2 Streaming 优化
// 核心思路:静态部分立即返回,动态部分流式加载
export default function DashboardPage() {
return (
<div>
{/* 立即返回的静态 Shell */}
<h1 className="text-2xl font-bold">Dashboard</h1>
<nav>...</nav>
{/* 独立流式加载 */}
<div className="grid grid-cols-3 gap-4 mt-8">
<Suspense fallback={<CardSkeleton />}>
<RevenueCard /> {/* 可能 200ms */}
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<ActiveUsersCard /> {/* 可能 300ms */}
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<RecentActivityCard /> {/* 可能 500ms */}
</Suspense>
</div>
{/* 较慢的数据独立加载 */}
<Suspense fallback={<TableSkeleton />}>
<ProjectTable /> {/* 可能 1s */}
</Suspense>
</div>
)
}3.3 PPR(部分预渲染)
// next.config.ts
export default { experimental: { ppr: true } }
// 效果:
// 1. 静态 shell(布局、导航、标题)在边缘缓存,毫秒级返回
// 2. 动态数据通过 Streaming 填充
// 3. TTFB 接近静态页面,内容实时更新3.4 Server Component vs Client Component 性能影响
Server Component:
✅ 不增加 JS Bundle(代码不发送到客户端)
✅ 直接访问数据库(无网络延迟)
✅ 自动流式渲染
❌ 不能有交互(onClick, useState)
Client Component:
❌ 增加 JS Bundle
❌ 需要 Hydration(TTI 延迟)
✅ 可以交互
结论:
Server Component 尽可能多用
Client Component 尽可能小
'use client' 边界推到叶子节点
4. Core Web Vitals 达标
4.1 三大核心指标
| 指标 | 全称 | 含义 | 达标值 |
|---|---|---|---|
| LCP | Largest Contentful Paint | 最大内容绘制时间 | ≤ 2.5s |
| INP | Interaction to Next Paint | 交互到下次绘制延迟 | ≤ 200ms |
| CLS | Cumulative Layout Shift | 累积布局偏移 | ≤ 0.1 |
4.2 LCP 优化
LCP 通常由最大的图片或文本块决定:
// 1. 首屏图片加 priority
import Image from 'next/image'
<Image src="/hero.jpg" priority alt="Hero" width={1200} height={630} />
// 2. 字体优化(避免 FOIT/FOUT)
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], display: 'swap' })
<body className={inter.className}>
// 3. 减少服务端响应时间
// - 并行数据获取 (Promise.all)
// - 数据库查询优化(索引、减少字段)
// - 使用缓存(unstable_cache, ISR)
// 4. 预加载关键资源
// Next.js 自动为 <Link> 预取,确保重要链接在视口内4.3 INP 优化
INP 衡量用户交互(点击、输入)后页面的响应速度:
// 1. 避免主线程长任务
// ❌ 同步处理大数据
function handleFilter(items: Item[]) {
const filtered = items.filter(/* 复杂过滤 */) // 可能阻塞 100ms+
setItems(filtered)
}
// ✅ 用 startTransition 降低优先级
function handleFilter(items: Item[]) {
startTransition(() => {
const filtered = items.filter(/* 复杂过滤 */)
setItems(filtered)
})
}
// 2. useOptimistic 即时反馈
// 点击后立即在 UI 更新,后台异步提交
// 3. 减少 Hydration 负担
// - 减少 Client Component 数量
// - 大列表用虚拟滚动(react-window)
// - 重交互组件 dynamic import4.4 CLS 优化
CLS 衡量页面元素意外移动的程度:
// 1. 图片和视频指定尺寸
<Image width={640} height={360} /> // ✅ 预留空间
<img src="..." /> // ❌ 加载后撑开布局
// 2. 字体加载不闪烁
const font = Inter({ display: 'swap' }) // swap 避免不可见文本
// 3. 动态内容预留空间
// ❌ 广告/通知条突然出现推挤内容
// ✅ 用 min-height 预留空间
<div className="min-h-[60px]">
<Suspense fallback={<Skeleton className="h-[60px]" />}>
<Banner />
</Suspense>
</div>
// 4. Skeleton 与实际内容尺寸一致
// Skeleton 的宽高应该和最终渲染的内容匹配4.5 性能监控
// 1. 使用 Next.js 内置的性能上报
// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next'
import { Analytics } from '@vercel/analytics/next'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<SpeedInsights />
<Analytics />
</body>
</html>
)
}
// 2. Web Vitals 自定义上报
// app/web-vitals.ts
export function reportWebVitals(metric: {
name: string
value: number
id: string
}) {
// 发送到你的分析服务
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify(metric),
})
}5. 数据库性能
5.1 查询优化 Checklist
## 数据库性能 Checklist
### 索引
- [ ] 所有外键字段有索引
- [ ] WHERE 子句常用字段有索引
- [ ] 复合查询有复合索引(如 tenantId + createdAt)
- [ ] 全文搜索使用 GIN 索引
### 查询模式
- [ ] 无 N+1 查询(循环中不查数据库)
- [ ] 使用 JOIN 替代多次查询
- [ ] 分页用 LIMIT + OFFSET 或 cursor
- [ ] 只 SELECT 需要的字段
- [ ] 大结果集用 streaming
### 连接管理
- [ ] 使用连接池(Neon Serverless / pgBouncer)
- [ ] Serverless 环境用 HTTP 连接(@neondatabase/serverless)
- [ ] 长连接环境用 TCP 连接池5.2 N+1 查询修复
// ❌ N+1:每个项目额外查一次成员数
const projects = await db.select().from(projectsTable)
for (const p of projects) {
p.memberCount = await db
.select({ count: count() })
.from(membersTable)
.where(eq(membersTable.projectId, p.id))
}
// ✅ 一次查询搞定
const projects = await db
.select({
id: projectsTable.id,
name: projectsTable.name,
memberCount: count(membersTable.id),
})
.from(projectsTable)
.leftJoin(membersTable, eq(membersTable.projectId, projectsTable.id))
.where(eq(projectsTable.tenantId, tenantId))
.groupBy(projectsTable.id)6. 综合 Checklist
按优先级排序的性能优化动作:
P0:必做(影响最大)
- [ ] Server Component 做数据获取(减少 JS Bundle + 消除 waterfall)
- [ ] 并行数据获取(Promise.all)
- [ ] 首屏图片加 priority
- [ ] 数据库常用查询字段有索引
- [ ] 消除 N+1 查询P1:推荐做(投入产出比高)
- [ ] Streaming + Suspense(独立加载区域)
- [ ] dynamic import 大组件(编辑器、图表)
- [ ] 数据库查询缓存(unstable_cache)
- [ ] 字体优化(next/font + display: swap)
- [ ] 图片优化(next/image + WebP/AVIF)P2:锦上添花
- [ ] PPR 部分预渲染(实验性)
- [ ] Bundle 分析 + 替换大依赖
- [ ] 第三方脚本延迟加载
- [ ] Web Vitals 持续监控
- [ ] 虚拟滚动(大列表)
- [ ] Edge Runtime(低延迟 API)本章小结
- Bundle:分析体积 → 替换大依赖 → dynamic import → 脚本延迟加载
- 缓存四层:Router Cache → Full Route Cache → Data Cache → Request Memoization
- 渲染策略:营销页 SSG、Dashboard SSR、博客 ISR,Streaming 解耦加载
- Core Web Vitals:LCP(图片 priority + 并行获取)、INP(startTransition)、CLS(固定尺寸 + Skeleton)
- 数据库:索引 + 消除 N+1 + 连接池 + 只查需要的字段
- 优先级:P0 五项必做 > P1 五项推荐 > P2 锦上添花