正在加载,请稍候…

Next.js 性能优化:图片、字体、缓存与包分析

实用的 Next.js 性能优化指南:Image 组件、字体优化、代码分割、包分析、缓存策略以及 Core Web Vitals 改进测量。

Next.js 性能优化:图片、字体、缓存与包分析

为什么 Next.js 性能需要刻意关注

Next.js 开箱即用提供了性能基础——自动代码分割、服务端渲染、图片优化。但“默认良好”并不意味着“针对你的应用优化”。

实际中的 Next.js 性能问题往往是自找的:未使用 <Image> 组件加载图片、字体导致布局偏移、因不良导入模式导致客户端包过大、以及缓存策略保持默认。

本指南涵盖了那些能对 Core Web Vitals 分数产生有意义影响的实用优化。

Next.js 性能优化:图片、字体、缓存与包分析插图

图片优化

图片通常是页面上最大的 Contentful Paint 元素。Next.js 的 <Image> 组件自动处理了困难部分:

import Image from 'next/image'

// 基本用法——Next.js 处理格式转换、尺寸调整、懒加载
<Image
  src="/hero.jpg"
  alt="Hero image"
  width={1200}
  height={630}
  priority  // 首屏图片应使用 priority(禁用懒加载)
/>

// 远程图片——在 next.config.js 中添加域名
<Image
  src="https://cdn.example.com/avatar.jpg"
  alt="User avatar"
  width={64}
  height={64}
  className="rounded-full"
/>

// Fill 模式——填充父容器
<div style={{ position: 'relative', height: '400px' }}>
  <Image
    src="/background.jpg"
    alt="Background"
    fill
    style={{ objectFit: 'cover' }}
    sizes="100vw"  // 对 fill 图片至关重要
  />
</div>

sizes 属性

这是最常被忽略的优化。没有 sizes,Next.js 不知道图片将渲染成多大:

// ❌ 没有 sizes——无论视口大小,Next.js 都提供全尺寸图片
<Image src="/card.jpg" alt="Card" width={400} height={300} />

// ✅ 有 sizes——提供适当尺寸的图片
<Image
  src="/card.jpg"
  alt="Card"
  width={400}
  height={300}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 400px"
  // 翻译为:“移动端全宽,平板端半宽,桌面端 400px”
/>

配置图片域名

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: '**.amazonaws.com', // 通配符子域名
      },
    ],
    // 自定义优化
    formats: ['image/avif', 'image/webp'], // 支持时提供 AVIF
    deviceSizes: [640, 828, 1080, 1200, 1920], // srcset 断点
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // 固定尺寸图片
  },
}

占位符与模糊

import Image from 'next/image'

// 静态导入——自动生成模糊数据 URL
import heroImage from '/public/hero.jpg'

<Image
  src={heroImage}
  alt="Hero"
  placeholder="blur"  // 加载时显示模糊
  priority
/>

// 动态图片——自行生成模糊占位符
<Image
  src={post.coverImage}
  alt={post.title}
  width={800}
  height={400}
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..." // 10px 模糊
/>

字体优化

字体导致累积布局偏移(CLS)并阻塞渲染。Next.js 的 next/font 消除了这两个问题:

// app/layout.tsx
import { Inter, Fira_Code } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',        // 防止字体加载时文本不可见
  variable: '--font-inter', // Tailwind 的 CSS 变量
})

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

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={`${inter.variable} ${firaCode.variable}`}>
      <body className={inter.className}>
        {children}
      </body>
    </html>
  )
}

next/font 在后台做了什么:

  1. 构建时下载字体(无运行时请求)
  2. 自托管并带有最佳缓存头
  3. 内联 @font-face 声明,使用 size-adjust 防止 CLS
  4. 生成 CSS 变量供 Tailwind 使用

Next.js 性能优化:图片、字体、缓存与包分析插图

本地字体

import localFont from 'next/font/local'

const brandFont = localFont({
  src: [
    {
      path: '../fonts/BrandFont-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: '../fonts/BrandFont-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
  ],
  variable: '--font-brand',
  display: 'swap',
})

包分析

在优化之前,先测量。Next.js 有内置的包分析器:

npm install @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
  // your config
})

# 运行分析
ANALYZE=true npm run build

常见包问题

1. 导入整个库而只需要一个函数:

// ❌ 导入整个 lodash(约 70KB gzipped)
import _ from 'lodash'
const result = _.groupBy(data, 'category')

// ✅ 只导入需要的(约 1KB)
import groupBy from 'lodash/groupBy'
const result = groupBy(data, 'category')

// ✅ 或使用原生替代
const result = data.reduce((acc, item) => {
  (acc[item.category] ??= []).push(item)
  return acc
}, {} as Record<string, typeof data>)

2. 日期库:

// ❌ moment.js = 67KB gzipped,包含所有语言环境
import moment from 'moment'

// ✅ date-fns = 可 tree-shake
import { format, parseISO } from 'date-fns'
// 或使用 Intl API 进行基本格式化(零字节!)
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(date)

3. 图标库:

// ❌ 导入整个图标集
import { IconSettings, IconUser } from '@tabler/icons-react'

// ✅ 相同——现代包是可 tree-shake 的
// 但通过包分析器验证 tree-shaking 是否生效

代码分割策略

Next.js 自动按路由进行代码分割。但路由内的大型组件仍会阻塞渲染:

import dynamic from 'next/dynamic'

// 懒加载重型组件
const MarkdownEditor = dynamic(() => import('./MarkdownEditor'), {
  loading: () => <p>Loading editor...</p>,
  ssr: false, // 如果是客户端专用(例如使用 DOM API),则不进行 SSR
})

// 条件加载
const AdminPanel = dynamic(() => import('./AdminPanel'))

function Page({ user }: { user: User }) {
  return (
    <div>
      <UserProfile user={user} />
      {user.isAdmin && <AdminPanel />} {/* 仅对管理员加载 */}
    </div>
  )
}

预加载

// 在需要之前预加载组件
const AdminPanel = dynamic(() => import('./AdminPanel'))

function Page({ user }: { user: User }) {
  // 悬停时预加载——常见 UX 模式
  function handleMouseEnter() {
    AdminPanel.preload?.()
  }
  
  return (
    <button onMouseEnter={handleMouseEnter} onClick={openAdmin}>
      Admin Panel
    </button>
  )
}

Next.js 性能优化:图片、字体、缓存与包分析插图

缓存策略

Next.js 14+ 具有分层缓存系统:

// 1. 静态渲染(构建时)——最快
// 没有动态数据的 page.tsx = 静态渲染
export default async function StaticPage() {
  const data = await fetch('https://api.example.com/static-data', {
    cache: 'force-cache' // 无限期缓存
  })
  return <Component data={data} />
}

// 2. ISR——按间隔重新验证
export const revalidate = 3600 // 每小时重新验证

export default async function IsrPage() {
  const posts = await fetch('/api/posts', {
    next: { revalidate: 3600 }
  }).then(r => r.json())
  
  return <PostList posts={posts} />
}

// 3. 按需重新验证
import { revalidatePath, revalidateTag } from 'next/cache'

export async function POST(request: Request) {
  // 由 webhook 在内容更改时调用
  await revalidatePath('/blog') // 重新验证特定路径
  await revalidateTag('posts')   // 重新验证所有带有此标签的 fetch
  
  return Response.json({ revalidated: true })
}

// 4. 动态(无缓存)——始终新鲜
export const dynamic = 'force-dynamic'
// 或:
const data = await fetch(url, { cache: 'no-store' })

测量 Core Web Vitals

// app/layout.tsx — Web Vitals 报告
'use client'

import { useReportWebVitals } from 'next/web-vitals'

export function WebVitals() {
  useReportWebVitals((metric) => {
    console.log(metric) // { id, name, value, rating }
    
    // 发送到分析
    if (metric.rating === 'poor') {
      analytics.track('poor_web_vital', {
        metric: metric.name,
        value: metric.value,
        page: window.location.pathname,
      })
    }
  })
  
  return null
}

关键指标

指标 良好 需要改进 较差
LCP (Largest Contentful Paint) < 2.5s 2.5–4.0s > 4.0s
INP (Interaction to Next Paint) < 200ms 200–500ms > 500ms
CLS (Cumulative Layout Shift) < 0.1 0.1–0.25 > 0.25
FCP (First Contentful Paint) < 1.8s 1.8–3.0s > 3.0s
TTFB (Time to First Byte) < 800ms 800ms–1.8s > 1.8s

实用优化清单

图片:
□ 所有图片使用 <Image> 组件
□ 首屏图片具有 priority 属性
□ 响应式图片具有正确的 sizes 属性
□ 重要图片使用 placeholder="blur"

字体:
□ 使用 next/font 而非 <link> 或 @import
□ 仅加载所需的子集和字重
□ 使用 font-display: swap

包:
□ 运行了包分析器,识别了最大的模块
□ 重型组件使用 dynamic() 导入
□ 没有完整的库导入(lodash、moment 等)
□ 第三方脚本使用 next/script 并指定 strategy

缓存:
□ 静态页面使用 force-cache
□ 动态页面明确指定缓存策略
□ 为偶尔变化的内容配置了 ISR
□ 为 CMS 驱动的内容配置了按需重新验证

性能:
□ 第三方脚本使用 next/script
□ 字体在 <head> 中预加载
□ 识别并优化了 Largest Contentful Paint 元素
□ CLS 分数低于 0.1(无字体/图片导致的布局偏移)

第三方脚本优化

import Script from 'next/script'

// 策略选项:
// - beforeInteractive:在页面水合之前加载(阻塞——尽量避免)
// - afterInteractive:在水合之后加载(默认——适合分析)
// - lazyOnload:在空闲时加载(适合非关键脚本)
// - worker:在 Web Worker 中加载(实验性——将 JS 移出主线程)

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        
        {/* 分析——在页面可交互后加载 */}
        <Script
          src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
          strategy="afterInteractive"
        />
        
        {/* 聊天小部件——在空闲时加载 */}
        <Script
          src="https://chat.example.com/widget.js"
          strategy="lazyOnload"
          onLoad={() => console.log('Chat loaded')}
        />
      </body>
    </html>
  )
}

Next.js 性能优化在于理解可用的调节旋钮并有意识地应用它们。最大的收益通常来自图片优化(使用带有正确 sizes<Image>)、消除字体导致的布局偏移(切换到 next/font),以及识别和分割大型客户端包。先测量,再优化。