Next.js: 图片优化与字体加载

最后更新:2026-08-26

图片和字体占页面体积的 70% 以上——优化它们能让 LCP 从 4s 降到 1s。

1. 你将学到


2. 一个前端性能工程师的真实故事

(1) 痛点:一张图片毁掉整页性能

Diana 是 TaskFlow 团队的 DevOps 工程师,她发现 Dashboard 页面的 LCP(Largest Contentful Paint)高达 4.8 秒:

"页面顶部 banner 是一张 5MB 的 PNG 图片(4000×3000px 原图),直接 <img src="/banner.png" />。Chrome DevTools 显示:解码耗时 800ms,布局偏移 CLS 0.45,字体加载阻塞渲染 600ms。"

问题 影响 CWV 指标
原图 5MB 未压缩 下载 3.2s ❌ LCP 4.8s
无尺寸属性 页面布局不断跳动 ❌ CLS 0.45
字体阻塞渲染 白屏延长 ❌ FCP 2.1s
无响应式图片 手机也加载 4K 图 ❌ 流量浪费

(2) next/image + next/font 的解法

<Image> 组件自动优化图片,next/font 消除字体阻塞。

TSX
import Image from 'next/image'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'], display: 'swap' })

export default function Hero() {
  return (
    <div className={inter.className}>
      <Image
        src="/banner.webp"
        alt="TaskFlow Banner"
        width={1200}
        height={400}
        priority
        placeholder="blur"
        blurDataURL="data:image/webp;base64,..."
      />
    </div>
  )
}

(3) 收益

指标 优化前 优化后 改善
LCP 4.8s 1.2s 75% ↓
CLS 0.45 0.02 96% ↓
图片体积 5 MB 120 KB 97% ↓
FCP 2.1s 0.8s 62% ↓

3. next/image 组件核心属性

(1) 属性速查表

属性 类型 必须 说明
src string / StaticImport 图片路径或静态导入
width number ✅(静态) 图片宽度(px)
height number ✅(静态) 图片高度(px)
alt string 替代文本(无障碍)
priority boolean LCP 图片预加载
placeholder 'blur' / 'empty' 加载中占位
blurDataURL string blur base64 模糊图
sizes string 响应式断点
fill boolean 填充父容器
quality number 压缩质量 (1-100)
loading 'lazy' / 'eager' 懒加载策略
100%
graph TB
    A[<Image src="/photo.jpg"/>] --> B{Next.js 构建}
    B --> C[Sharp 服务端重编码]
    C --> D[WebP 版本<br/>1200w / 800w / 400w]
    C --> E[AVIF 版本<br/>1200w / 800w / 400w]
    D --> F[浏览器选择<br/><picture> 自动适配]
    E --> F

    style B fill:#cce5ff
    style C fill:#d4edda
    style F fill:#fff3cd

(2) responsive 配置示例

TS
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'images.unsplash.com' },
      { protocol: 'https', hostname: 'cdn.taskflow.io', port: '', pathname: '/assets/**' }
    ],
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384]
  }
}

export default nextConfig
⚠️ 注意: images.domains 已在 Next.js 15+ 弃用,请使用 remotePatterns(支持通配符路径匹配)。

▶ 示例:远程图片 + 响应式 + blur 占位

TSX
// components/TeamPhoto.tsx
import Image from 'next/image'

export default function TeamPhoto() {
  return (
    <Image
      src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200"
      alt="TaskFlow 团队合照"
      width={1200}
      height={600}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 1200px"
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
      priority
      className="rounded-lg"
    />
  )
}
💡 提示: sizes 告诉浏览器不同视口下图片的显示宽度,帮助浏览器选择最合适尺寸的图片资源。不设置可能导致手机也加载 1920px 大图。


4. placeholder="blur" 与 blurDataURL

(1) 两种占位方案

方案 生成方式 体积 适用场景
静态导入 blur Next.js 自动生成 ~2KB 本地图片(import img from './photo.jpg'
手动 blurDataURL 工具生成 base64 ~200B 远程图片、动态图片
Plaiceholder 库 运行时生成 ~500B 需要动态 remote URL

(2) 静态导入自动 blur

TSX
// ✅ 静态导入 — Next.js 自动生成 blurDataURL
import teamPhoto from '@/public/team.jpg'

export default function AboutPage() {
  return (
    <Image
      src={teamPhoto}
      alt="团队照片"
      placeholder="blur"          // 自动使用生成的 blur
      className="rounded-xl"
    />
  )
}

(3) 远程图片 blur 预览生成工具

TEXT 📖 仅展示
# 使用 plaiceholder 库
npm install plaiceholder
TS
// lib/getBlurData.ts — 远程图片 blurURL 生成
import { getPlaiceholder } from 'plaiceholder'

export async function getBlurDataURL(src: string) {
  try {
    const response = await fetch(src)
    const buffer = Buffer.from(await response.arrayBuffer())
    const { base64 } = await getPlaiceholder(buffer)
    return base64
  } catch {
    return undefined
  }
}

▶ 示例:动态远程图片 + blur 占位

TSX
// app/team/page.tsx — 团队头像列表 + blur 占位
import Image from 'next/image'
import { getBlurDataURL } from '@/lib/getBlurData'

const members = [
  { name: 'Alice', avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200' },
  { name: 'Bob', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200' },
]

export default async function TeamPage() {
  const membersWithBlur = await Promise.all(
    members.map(async (m) => ({ ...m, blur: await getBlurDataURL(m.avatar) }))
  )

  return (
    <div style={{ display: 'flex', gap: '1rem' }}>
      {membersWithBlur.map((m) => (
        <Image
          key={m.name}
          src={m.avatar}
          alt={m.name}
          width={100}
          height={100}
          placeholder="blur"
          blurDataURL={m.blur}
          className="rounded-full"
        />
      ))}
    </div>
  )
}

5. next/font 字体优化

(1) 字体加载对性能的影响

100%
graph TB
    A[传统 @font-face] --> B[阻塞渲染<br/>FOIT]
    A --> C[布局偏移<br/>FOUT]
    B --> D[FCP 延迟 300-600ms]
    C --> E[CLS 0.1-0.3]

    F[next/font] --> G[display:swap<br/>立即使用后备字体]
    F --> H[size-adjust<br/>消除布局偏移]
    F --> I[preload 预加载<br/>关键路径无阻塞]
    F --> J[CSS size-adjust<br/>字体度量覆盖]

    style A fill:#f8d7da
    style F fill:#d4edda
问题 传统字体加载 next/font 解决
FOIT(字体不可见) 字体下载前不显示文字 display:swap 立即显示后备字体
CLS(布局偏移) 字体加载前后尺寸变化 size-adjust 覆盖字体度量
额外请求 多个字体文件串行下载 CSS 内联 + 预加载
Google Fonts 延迟 跨国 CDN 请求慢 构建时下载,零运行时请求

(2) Google 可变字体

TSX
// app/layout.tsx — Google 可变字体
import { Inter, Roboto_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,
  variable: '--font-inter',         // CSS 变量模式
  weight: 'variable'                // 可变字体范围
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono'
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh" className={`${inter.variable} ${robotoMono.variable}`}>
      <body style={{ fontFamily: 'var(--font-inter)' }}>
        <code style={{ fontFamily: 'var(--font-roboto-mono)' }}>{children}</code>
      </body>
    </html>
  )
}

(3) 字体大小调整(size-adjust)

TSX
// 自定义本地字体 + size-adjust 防 CLS
import localFont from 'next/font/local'

const myFont = localFont({
  src: './fonts/CustomFont.woff2',
  display: 'swap',
  adjustment: {
    ascent: 90,
    descent: 20,
    lineGap: 10,
    sizeAdjust: '105%'
  }
})

▶ 示例:多字体组合 + Tailwind CSS

TSX
// app/layout.tsx — 业务字体 + 代码字体 + Tailwind
import { Inter, JetBrains_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-sans',
  display: 'swap'
})

const jetbrainsMono = JetBrains_Mono({
  subsets: ['latin'],
  variable: '--font-mono',
  display: 'swap'
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh" className={`${inter.variable} ${jetbrainsMono.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  )
}
CSS
/* tailwind.config.ts */
import type { Config } from 'tailwindcss'

export default {
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-sans)'],
        mono: ['var(--font-mono)']
      }
    }
  }
} satisfies Config

6. 图片优化原理:Sharp + WebP/AVIF

(1) Sharp 优化流水线

100%
graph LR
    A[源图片<br/>PNG/JPEG 5MB] --> B[Sharp 服务端处理]
    B --> C[调整尺寸<br/>1200w / 800w / 400w]
    B --> D[质量压缩<br/>75% quality]
    B --> E[格式转换]
    E --> F[WebP<br/>~80% 体积↓]
    E --> G[AVIF<br/>~90% 体积↓]
    F --> H[<picture> 自动协商<br/>浏览器选最优格式]

    style B fill:#d4edda
    style H fill:#cce5ff
格式 压缩率(vs JPEG) 浏览器支持 解码速度
JPEG 基准 100% 最快
WebP ~30% 体积 ↓ 96% 较快
AVIF ~50% 体积 ↓ 93% 较慢
HEIC ~50% 体积 ↓ 仅 Safari 较快

(2) 配置全局图片质量

TS
// next.config.ts — 全局图片优化配置
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60 * 60 * 24 * 30,  // 30 天缓存
    dangerouslyAllowSVG: false,
    contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;"
  }
}

▶ 示例:图片质量与尺寸测试

TSX
// components/ImageTest.tsx — 同一图片不同质量对比
import Image from 'next/image'

export default function ImageTest() {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
      <div>
        <h3>Quality=50</h3>
        <Image src="/test.jpg" alt="q50" width={400} height={300} quality={50} />
      </div>
      <div>
        <h3>Quality=75(默认)</h3>
        <Image src="/test.jpg" alt="q75" width={400} height={300} quality={75} />
      </div>
      <div>
        <h3>Quality=100</h3>
        <Image src="/test.jpg" alt="q100" width={400} height={300} quality={100} />
      </div>
    </div>
  )
}
💡 提示: 肉眼几乎看不出 75% 和 100% 的差异,但体积相差 3-4 倍。生产环境推荐 75-85%。


7. Core Web Vitals 实战调优

(1) CWV 三大指标

指标 满分 优化手段
LCP(最大内容绘制) ≤2.5s >4.0s 预加载 LCP 图片 + 压缩 + CDN
CLS(累计布局偏移) ≤0.1 >0.25 固定图片尺寸 + 字体 size-adjust
INP(交互到下次绘制) ≤200ms >500ms 减少 JS 执行 + 代码分割

(2) LCP 图片优化清单

TSX
// ✅ 正确的 LCP 图片配置
export default function HeroBanner() {
  return (
    <Image
      src="/hero-banner.webp"
      alt="TaskFlow Hero"
      width={1440}
      height={600}
      priority          // 告诉 Next.js 预加载这张图
      quality={85}
      placeholder="blur"
      sizes="100vw"
      style={{ width: '100%', height: 'auto' }}  // 响应式
    />
  )
}

(3) 字体 CLS 消除

TSX
// layout.tsx — 字体 CLS 消除完整方案
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  adjustFontFallback: true,   // Next.js 自动计算 size-adjust
  fallback: ['system-ui', 'sans-serif']
})
🔥 易错: 不要在同一页面上加载超过 3 种字体权重。每个变体会额外增加 ~50KB 的 WOFF2 文件。

▶ 示例:CWV 监控组件

TSX
// components/WebVitals.tsx — 上报 CWV 到分析平台
'use client'
import { useReportWebVitals } from 'next/web-vitals'

export default function WebVitalsReporter() {
  useReportWebVitals((metric) => {
    console.log(metric)  // 开发时查看

    // 生产环境上报到分析 API
    if (process.env.NODE_ENV === 'production') {
      const body = JSON.stringify({
        name: metric.name,
        value: metric.value,
        rating: metric.rating,
        id: metric.id
      })
      navigator.sendBeacon('/api/analytics', body)
    }
  })

  return null
}
TSX
// app/layout.tsx — 挂载监控
import WebVitalsReporter from '@/components/WebVitals'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <WebVitalsReporter />
      </body>
    </html>
  )
}

8. 完整示例:首页图片 + 字体 + CWV 综合优化

TSX
// app/layout.tsx — 根布局,全局字体 + 图片 + Vitals 监控
import { Inter, Noto_Sans_SC } from 'next/font/google'
import WebVitalsReporter from '@/components/WebVitals'
import type { Metadata } from 'next'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
  adjustFontFallback: true
})

const notoSansSC = Noto_Sans_SC({
  subsets: ['latin'],
  weight: 'variable',
  variable: '--font-noto',
  display: 'swap',
  adjustFontFallback: true
})

export const metadata: Metadata = {
  title: 'TaskFlow - 团队协作平台',
  description: 'TaskFlow 帮助 10,000+ 团队高效协同工作'
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh" className={`${inter.variable} ${notoSansSC.variable}`}>
      <body style={{ fontFamily: 'var(--font-inter), var(--font-noto), sans-serif' }}>
        {children}
        <WebVitalsReporter />
      </body>
    </html>
  )
}
TSX
// app/page.tsx — 首页 Hero + LCP 优化
import Image from 'next/image'
import Link from 'next/link'

export default function HomePage() {
  return (
    <div>
      {/* Hero 区域 — LCP 图片 */}
      <div style={{ position: 'relative', width: '100%', height: 500 }}>
        <Image
          src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1920&q=85"
          alt="TaskFlow 团队协作"
          fill
          priority
          sizes="100vw"
          style={{ objectFit: 'cover' }}
          placeholder="blur"
          blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
        />
        <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <div style={{ textAlign: 'center', color: '#fff' }}>
            <h1 style={{ fontSize: 'clamp(2rem, 5vw, 4rem)' }}>TaskFlow</h1>
            <p style={{ fontSize: '1.25rem' }}>10,000+ 团队正在使用的协作平台</p>
            <Link href="/signup" style={{ display: 'inline-block', padding: '0.75rem 2rem', background: '#4f46e5', color: '#fff', borderRadius: 8 }}>
              免费开始
            </Link>
          </div>
        </div>
      </div>

      {/* Feature 区域 — 懒加载图片 */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '2rem', padding: '4rem 2rem' }}>
        {features.map((feature) => (
          <div key={feature.title}>
            <Image
              src={feature.icon}
              alt={feature.title}
              width={48}
              height={48}
              loading="lazy"
            />
            <h3>{feature.title}</h3>
            <p>{feature.description}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

const features = [
  { title: '项目管理', description: '看板 + 甘特图', icon: '/icons/project.svg' },
  { title: '实时协作', description: '多人同时编辑', icon: '/icons/team.svg' },
  { title: '智能分析', description: 'AI 驱动的洞察', icon: '/icons/analytics.svg' },
]

❓ 常见问题

Q next/imagefill 和静态 width/height 有什么区别?
A fill 让图片填充父容器(父需 position: relative),不指定固定尺寸,配合 objectFit 使用。静态宽高需明确给出图片宽高,用于计算占位空间防止 CLS。
Q placeholder="blur" 的模糊图怎么生成?
A 静态导入时 Next.js 自动生成。远程图片需要手动提供 blurDataURL(base64 模糊缩略图),可用 plaiceholder 库或在线工具生成(如 https://png-pixel.com 生成 4x4 模糊图)。
Q Google Fonts 的 display=swapnext/fontdisplay:swap 是一回事吗?
A 效果相同但实现不同。传统方法在 <link> 标签加 &display=swap,字体下载完成后才替换。next/font 在构建时下载字体文件,CSS 内联到页面,零运行时请求,同时自动生成 size-adjust 消除 CLS。
Q 为什么我的 LCP 图片没有自动预加载?
A 检查 priority 属性是否设置在 LCP 图片上。Next.js 只对带 priority 的图片添加 <link rel="preload">。另外 remotePatterns 配置不正确也会导致优化失效。
Q AVIF 和 WebP 应该选哪个?
A 两者都配置,通过 <picture> 标签让浏览器自动选择。AVIF 压缩率更高但解码慢,适合非 LCP 图片;WebP 兼容性更好(96%),适合 LCP 图片。Next.js 默认生成两种格式。

📖 小节


📝 作业

  1. 基础题(⭐):在项目中配置 next/image,使用 remotePatterns 允许 Unsplash 图片源,实现一个响应式图片组件(含 sizesplaceholder="blur")。

  2. 进阶题(⭐⭐):对比不用的图片质量(50/75/100)和格式(JPEG/WebP/AVIF)对页面 LCP 的影响,记录每种组合的体积和加载时间,总结最佳配置。

  3. 挑战题(⭐⭐⭐):构建一个完整的 CWV 优化方案:next/font 配置两种可变字体(正文字体 + 代码字体)、LCP 图片配置 priority + blur 占位、useReportWebVitals 上报指标到自定义 API、最终实现 Lighthouse Performance 分数 ≥ 90。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏