性能调优实战 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
# 自动打开浏览器展示 treemap

1.2 常见体积问题

问题影响修复
Lodash 全量导入+70KBimport debounce from 'lodash/debounce'
Moment.js+300KBdate-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 三大核心指标

指标全称含义达标值
LCPLargest Contentful Paint最大内容绘制时间≤ 2.5s
INPInteraction to Next Paint交互到下次绘制延迟≤ 200ms
CLSCumulative 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 import

4.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 锦上添花