Chapter 09

性能优化与 SEO

next/image、next/font、Metadata API 与 Bundle 分析的完整实践

9.1 next/image 图片优化

Next.js 内置的 Image 组件是对原生 <img> 标签的增强封装,自动处理图片优化的方方面面。使用 next/image 是 Next.js 中图片处理的强制最佳实践

import Image from 'next/image'

// 本地图片(自动获取尺寸)
import heroImage from '@/public/hero.jpg'

function HeroSection() {
  return (
    <Image
      src={heroImage}
      alt="Hero 图片"
      priority         {/* LCP 关键图片,优先加载 */}
      placeholder="blur" {/* 模糊占位符 */}
    />
  )
}

// 远程图片(需要在 next.config.ts 添加域名白名单)
function UserAvatar({ url }: { url: string }) {
  return (
    <Image
      src={url}
      alt="用户头像"
      width={48}
      height={48}
      sizes="48px"
      className="rounded-full"
    />
  )
}

// 填充父容器(需要父元素 position: relative)
function CoverImage({ src }: { src: string }) {
  return (
    <div style={{ position: 'relative', height: '300px' }}>
      <Image
        src={src}
        alt="封面"
        fill
        sizes="(max-width: 768px) 100vw, 50vw"
        style={{ objectFit: 'cover' }}
      />
    </div>
  )
}
TSX

9.2 next/font 字体优化

字体是影响页面渲染性能的重要因素。next/font 在构建时下载字体文件,将其内联到 CSS 中,消除了对 Google Fonts 等外部服务的运行时请求,避免了字体加载导致的布局位移(FOUT/FOIT)。

// app/layout.tsx — 字体配置
import { Inter, Noto_Sans_SC, Fira_Code } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',      // 暴露为 CSS 变量
  display: 'swap',               // 字体加载策略
})

const notoSansSC = Noto_Sans_SC({
  subsets: ['latin'],
  weight: ['400', '600', '700'],  // 只加载需要的字重
  variable: '--font-noto',
})

const firaCode = Fira_Code({
  subsets: ['latin'],
  variable: '--font-code',
})

export default function RootLayout({ children }) {
  return (
    <html lang="zh-CN" className=`${inter.variable} ${notoSansSC.variable} ${firaCode.variable}`>
      <body>{children}</body>
    </html>
  )
}
TSX
/* globals.css — 使用 CSS 变量应用字体 */
body {
  font-family: var(--font-noto), var(--font-inter), sans-serif;
}
code, pre {
  font-family: var(--font-code), monospace;
}
CSS

9.3 Metadata API — SEO 最佳实践

Next.js 15 提供了基于导出的 Metadata API,替代了传统的 <Head> 组件。支持静态导出 metadata 对象和动态生成的 generateMetadata 函数,自动处理 <title><meta>、Open Graph、Twitter Card 等 SEO 标签。

// app/layout.tsx — 根 Metadata(默认值)
import type { Metadata } from 'next'

export const metadata: Metadata = {
  // title 模板:子页面会显示 "文章标题 | My Blog"
  title: {
    template: '%s | My Blog',
    default:  'My Blog',
  },
  description: '一个用 Next.js 构建的博客',
  metadataBase: new URL('https://myblog.com'),
  openGraph: {
    type:        'website',
    siteName:    'My Blog',
    locale:      'zh_CN',
    images: [{ url: '/og-default.png', width: 1200, height: 630 }],
  },
  twitter: {
    card:    'summary_large_image',
    creator: '@yourhandle',
  },
  robots: {
    index:  true,
    follow: true,
    googleBot: { index: true, follow: true },
  },
}
TS
// app/blog/[slug]/page.tsx — 动态 Metadata
import type { Metadata, ResolvingMetadata } from 'next'

export async function generateMetadata(
  { params }: { params: Promise<{ slug: string }> },
  parent: ResolvingMetadata
): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)

  if (!post) return { title: '文章不存在' }

  // 继承父级 openGraph 配置
  const parentOG = (await parent).openGraph?.images ?? []

  return {
    title:       post.title,
    description: post.excerpt,
    openGraph: {
      title:  post.title,
      description: post.excerpt,
      images: [
        { url: post.coverImage, width: 1200, height: 630 },
        ...parentOG,
      ],
    },
    alternates: {
      canonical: `/blog/${slug}`,
    },
  }
}
TS

9.4 generateStaticParams — 静态路由预渲染

对于动态路由(如 /blog/[slug]),使用 generateStaticParams 告诉 Next.js 在构建时预渲染哪些参数组合。这相当于 Pages Router 中的 getStaticPaths,但语法更简洁。

预渲染的好处是:这些页面在构建时生成静态 HTML,部署后直接从 CDN 边缘节点提供服务,无需服务器计算,首字节时间(TTFB)极短。

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  // 在构建时获取所有文章 slug
  const posts = await db.post.findMany({
    where:  { published: true },
    select: { slug: true },
  })

  // 返回参数数组,Next.js 会为每个 slug 预渲染一个页面
  return posts.map(post => ({ slug: post.slug }))
}

// dynamicParams: false — 未预渲染的路由返回 404
// dynamicParams: true(默认)— 未预渲染的路由按需 SSR
export const dynamicParams = true
TS

9.5 Bundle 分析与优化

Bundle 大小直接影响页面加载性能。Next.js 提供了官方的 Bundle 分析工具,可以可视化每个页面的 JavaScript 依赖构成,找出体积过大的依赖包进行优化。

# 安装分析工具
pnpm add -D @next/bundle-analyzer

# 分析构建(生成可视化报告)
ANALYZE=true pnpm build
SHELL
// next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer'

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
})

export default withBundleAnalyzer({
  // 你的 next.config 配置
})
TS

常见 Bundle 优化技巧

// 1. 动态导入(代码分割,按需加载)
import dynamic from 'next/dynamic'

const HeavyChart = dynamic(() => import('@/components/chart'), {
  loading: () => <p>加载图表中...</p>,
  ssr: false,  // 图表库通常需要 window,禁用 SSR
})

// 2. 只导入需要的模块(避免全量导入)
// ❌ 导入整个 lodash(~70KB)
import _ from 'lodash'
// ✅ 只导入需要的函数(~2KB)
import debounce from 'lodash/debounce'

// 3. 使用 Tree-shaking 友好的包
// ❌ date-fns 旧版(需要手动按需引入)
// ✅ date-fns v3 / day.js(默认支持 tree-shaking)
import { format } from 'date-fns'

// 4. 服务端依赖放在 Server Component(不进入 Bundle)
// marked、gray-matter、prisma 等只在 Server Component 使用,
// 它们的代码永远不会出现在客户端 Bundle 中
TSX

9.6 Core Web Vitals 优化总结

Google 的 Core Web Vitals(核心网页指标)是衡量用户体验的关键指标,直接影响 SEO 排名。

指标 全称 目标值 Next.js 优化手段
LCP 最大内容绘制 <2.5s Image priority、字体预加载、SSR/SSG
INP 交互到下一帧绘制 <200ms useTransition、Server Actions、减少 JS 执行
CLS 累计布局位移 <0.1 Image width/height、字体 display:swap、骨架屏
FCP 首次内容绘制 <1.8s 流式渲染、边缘部署、HTTP/2 Push
💡

使用 Vercel Speed Insights在 Vercel 部署后,在 layout.tsx 中添加 <SpeedInsights />(来自 @vercel/speed-insights/next),即可实时监控真实用户的 Core Web Vitals 数据,指导针对性优化。