Next.js: 图片优化与字体加载
最后更新:2026-08-26
图片和字体占页面体积的 70% 以上——优化它们能让 LCP 从 4s 降到 1s。
1. 你将学到
next/image<Image>组件的核心属性(remotePatterns/sizes/priority/placeholder)- 图片优化原理:Sharp 服务端重编码、WebP / AVIF 自适应
next/font字体优化(Google 可变字体、display:swap、size-adjust、预加载)- Core Web Vitals(LCP / CLS / INP)的性能调优策略
- 远程图片安全与
images.domains弃用迁移
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' |
❌ | 懒加载策略 |
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) 字体加载对性能的影响
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 优化流水线
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/image 的 fill 和静态 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=swap 和 next/font 的 display: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 默认生成两种格式。📖 小节
next/image提供自动优化:Sharp 重编码、WebP/AVIF 自适应、响应式多尺寸、lazy loadingremotePatterns替代弃用的images.domains,支持路径通配符安全匹配placeholder="blur"结合blurDataURL消除图片加载时的空白闪烁next/font在构建时下载 Google 字体,消除 FOIT 和 FOUT,自动调整字体度量防 CLS- LCP 图片必须设置
priority预加载;非首屏图片用loading="lazy"延迟加载 - Core Web Vitals 调优核心:图片压缩(LCP)、固定尺寸(CLS)、JS 代码分割(INP)
📝 作业
-
基础题(⭐):在项目中配置
next/image,使用remotePatterns允许 Unsplash 图片源,实现一个响应式图片组件(含sizes和placeholder="blur")。 -
进阶题(⭐⭐):对比不用的图片质量(50/75/100)和格式(JPEG/WebP/AVIF)对页面 LCP 的影响,记录每种组合的体积和加载时间,总结最佳配置。
-
挑战题(⭐⭐⭐):构建一个完整的 CWV 优化方案:
next/font配置两种可变字体(正文字体 + 代码字体)、LCP 图片配置priority+blur占位、useReportWebVitals上报指标到自定义 API、最终实现 Lighthouse Performance 分数 ≥ 90。