图片与字体优化
图片和字体是网页中最大的两类资源。一张未优化的 Hero 图片可能 2MB,一套中文字体可能 10MB——直接拖垮 LCP。Next.js 内置了
next/image和next/font两个组件,从根源上解决这些问题。本章讲清图片优化的完整策略(格式、尺寸、懒加载、CDN)和字体优化(子集化、预加载、零 CLS)。
1. next/image 深度配置
1.1 为什么不能直接用 <img>
原生 <img> 标签有三个致命问题:
- 没有自动缩放:上传一张 4000×3000 的照片,即使显示区域只有 400×300,浏览器也会下载完整尺寸
- 没有格式转换:JPEG 文件即使浏览器支持 WebP/AVIF,也不会自动转换为更小的格式
- 没有尺寸占位:图片加载前没有预留空间,加载后会把下方内容推开——造成 CLS(布局偏移)
next/image 一次性解决了这三个问题。它不是简单的封装,而是一个图片优化管线:请求到达时,服务端根据浏览器的 Accept 头和请求的尺寸,按需生成最优格式和尺寸的图片,并缓存结果。
// ❌ 原生 img — 下载完整 4000×3000 JPEG
<img src="/hero.jpg" alt="Hero" />
// ✅ next/image — 自动缩放到 800×600,转为 WebP,预留空间
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero"
width={800}
height={600}
/>1.2 本地图片 vs 远程图片
本地图片(从 import 导入):Next.js 在构建时自动读取图片尺寸,无需手动指定 width/height:
import heroImage from '@/public/images/hero.jpg'
// width 和 height 自动从文件读取,还会生成 blur 占位图
<Image
src={heroImage}
alt="Hero banner"
placeholder="blur" // 自动生成的模糊占位图,消除 CLS
/>远程图片(URL 地址):必须手动指定尺寸,或用 fill 模式让图片填满父容器。同时需要在 next.config.ts 中配置允许的域名——这是安全措施,防止你的服务器被当作图片代理:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
{
protocol: 'https',
hostname: '*.amazonaws.com', // 支持通配符
},
],
},
}
export default nextConfig// 远程图片 — 必须指定尺寸
<Image
src="https://cdn.example.com/images/photo.jpg"
alt="Remote photo"
width={800}
height={600}
/>
// 或者用 fill 模式 — 填满父容器,父容器需要 position: relative
<div className="relative w-full h-[400px]">
<Image
src="https://cdn.example.com/images/photo.jpg"
alt="Remote photo"
fill
className="object-cover" // 控制裁剪方式
/>
</div>1.3 关键属性详解
<Image
src={heroImage}
alt="Hero banner"
// 尺寸控制
width={800} // 渲染宽度(像素)
height={600} // 渲染高度(像素)
fill // 或者用 fill 填满父容器(不能和 width/height 同时用)
sizes="(max-width: 768px) 100vw, 50vw" // 响应式尺寸提示(见第 4 节)
// 加载行为
priority // 关键图片:禁用懒加载 + preload(用于首屏 LCP 图片)
loading="lazy" // 默认行为:滚动到视口附近才加载
placeholder="blur" // 占位效果:blur(模糊)或 empty(空白)
blurDataURL="data:..." // 远程图片的自定义模糊占位
// 质量
quality={85} // 压缩质量 1-100,默认 75。85 是视觉无损的甜点
// 样式
className="rounded-lg" // 直接传 className
style={{ objectFit: 'cover' }}
/>priority 是 LCP 优化的关键:默认所有图片都是懒加载。但首屏的 Hero 图片需要立即加载,给它加 priority 会在 <head> 中注入 <link rel="preload">,让浏览器尽早开始下载。一个页面最多给 1-2 张图片加 priority。
1.4 图片加载器(Loader)
默认情况下,Next.js 用内置的图片优化服务器(/_next/image)处理图片。在 Vercel 部署时这很好用。但如果你用 Cloudflare、AWS 或自托管,可能需要自定义 Loader:
// lib/image-loader.ts
import type { ImageLoaderProps } from 'next/image'
// Cloudflare Images 加载器
export function cloudflareLoader({ src, width, quality }: ImageLoaderProps): string {
return `https://imagedelivery.net/your-account-hash/${src}/w=${width},q=${quality || 85}`
}
// 阿里云 OSS 图片处理
export function ossLoader({ src, width, quality }: ImageLoaderProps): string {
return `${src}?x-oss-process=image/resize,w_${width}/quality,q_${quality || 85}/format,webp`
}import Image from 'next/image'
import { cloudflareLoader } from '@/lib/image-loader'
<Image
loader={cloudflareLoader}
src="photo-id-123"
alt="Photo"
width={800}
height={600}
/>全局配置(所有图片都用同一个 Loader):
// next.config.ts
const nextConfig: NextConfig = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.ts',
},
}2. next/font 字体优化
2.1 Web 字体的性能问题
Web 字体不优化有三个严重问题:
- FOIT(Flash of Invisible Text):字体下载期间文字不可见——用户看到空白区域
- FOUT(Flash of Unstyled Text):先用系统字体显示,字体下载完后切换——造成明显的闪烁和 CLS
- 体积巨大:一套完整的 Google Fonts 中文字体可能超过 10MB
next/font 的解决方案:
- 在构建时下载字体文件,打包到你的域名下——不再需要运行时连接 Google Fonts CDN
- 使用 CSS
size-adjust技术让回退字体和目标字体的度量一致——消除 CLS - 自动启用
font-display: swap——文字立即可见(用系统字体),字体下载完无缝切换
2.2 Google Fonts
// app/layout.tsx
import { Inter, Noto_Sans_SC } from 'next/font/google'
// 英文字体
const inter = Inter({
subsets: ['latin'], // 只加载拉丁字符子集(~15KB,完整字体 ~300KB)
display: 'swap', // 字体加载前先用系统字体
variable: '--font-inter', // 注入 CSS 变量(推荐方式)
})
// 中文字体 — 会自动进行子集化和分片加载
const notoSansSC = Noto_Sans_SC({
subsets: ['latin'], // 中文字体无需指定 chinese 子集,next/font 会自动处理
weight: ['400', '500', '700'],
display: 'swap',
variable: '--font-noto',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN" className={`${inter.variable} ${notoSansSC.variable}`}>
<body>{children}</body>
</html>
)
}在 Tailwind CSS 中使用:
// tailwind.config.ts
import type { Config } from 'tailwindcss'
const config: Config = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)', 'var(--font-noto)', 'system-ui', 'sans-serif'],
mono: ['var(--font-jetbrains)', 'Menlo', 'monospace'],
},
},
},
}
export default config2.3 本地字体
对于自定义字体或需要精确控制的场景:
import localFont from 'next/font/local'
// 单个文件
const customFont = localFont({
src: './fonts/CustomFont.woff2', // 相对于当前文件的路径
display: 'swap',
variable: '--font-custom',
})
// 多粗细(Variable Font 最优——一个文件包含所有粗细)
const variableFont = localFont({
src: './fonts/InterVariable.woff2',
display: 'swap',
variable: '--font-inter',
weight: '100 900', // 范围声明
})
// 多文件(非 Variable Font 时需要分别指定)
const multiWeightFont = localFont({
src: [
{ path: './fonts/Font-Regular.woff2', weight: '400', style: 'normal' },
{ path: './fonts/Font-Medium.woff2', weight: '500', style: 'normal' },
{ path: './fonts/Font-Bold.woff2', weight: '700', style: 'normal' },
{ path: './fonts/Font-Italic.woff2', weight: '400', style: 'italic' },
],
display: 'swap',
variable: '--font-multi',
})2.4 中文字体优化策略
中文字体的挑战:常用汉字就有 6000+ 个,完整字体文件 5-20MB。三种策略:
方案一:系统字体栈(零成本,推荐大多数项目)
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;方案二:Google Fonts 自动分片(next/font 默认行为)
next/font 对中文 Google Fonts 会自动按 Unicode Range 分片加载——只下载页面实际用到的字符对应的分片。一个分片通常只有几十 KB。
方案三:自定义子集化(极致优化)
如果你确切知道页面用到哪些字符(如 Landing Page 的标题),可以用 fonttools 工具提取子集:
# 安装 fonttools
pip install fonttools brotli
# 只保留指定字符
pyftsubset NotoSansSC-Regular.ttf \
--text="让创意自由生长" \
--output-file=NotoSansSC-Subset.woff2 \
--flavor=woff23. 图片格式选择
3.1 现代格式对比
| 格式 | 压缩比 | 浏览器支持 | 适用场景 |
|---|---|---|---|
| JPEG | 基准 | 100% | 照片、渐变 |
| PNG | 无损,较大 | 100% | 透明图、截图 |
| WebP | 比 JPEG 小 25-35% | 97%+ | 通用推荐 |
| AVIF | 比 JPEG 小 50%+ | 92%+ | 追求极致压缩 |
| SVG | 极小(矢量) | 100% | 图标、Logo、插图 |
next/image 默认行为:检查浏览器的 Accept 请求头,支持 AVIF 就返回 AVIF,支持 WebP 就返回 WebP,否则返回原格式。你不需要手动处理格式转换。
3.2 配置格式优先级
// next.config.ts
const nextConfig: NextConfig = {
images: {
formats: ['image/avif', 'image/webp'], // 优先级:AVIF > WebP > 原格式
// 默认就是这个配置。如果你的用户浏览器较旧,可以只保留 WebP:
// formats: ['image/webp'],
},
}3.3 静态资源的手动优化
对于不经过 next/image 的静态资源(如 OG Image、邮件模板图片),在构建流程中手动优化:
// scripts/optimize-images.mts
import sharp from 'sharp'
import { readdir, stat } from 'fs/promises'
import { join, parse } from 'path'
async function optimizeImages(dir: string) {
const files = await readdir(dir)
for (const file of files) {
const filePath = join(dir, file)
const fileStat = await stat(filePath)
if (fileStat.isDirectory()) {
await optimizeImages(filePath)
continue
}
const { ext, name } = parse(file)
if (!['.jpg', '.jpeg', '.png'].includes(ext.toLowerCase())) continue
const outputWebP = join(dir, `${name}.webp`)
const outputAVIF = join(dir, `${name}.avif`)
// WebP — 兼容性好
await sharp(filePath)
.webp({ quality: 85 })
.toFile(outputWebP)
// AVIF — 更小但编码更慢
await sharp(filePath)
.avif({ quality: 70 }) // AVIF 的 70 质量 ≈ WebP 的 85 质量
.toFile(outputAVIF)
const original = fileStat.size
const webpStat = await stat(outputWebP)
const avifStat = await stat(outputAVIF)
console.log(`${file}: ${(original / 1024).toFixed(0)}KB → WebP ${(webpStat.size / 1024).toFixed(0)}KB (${((1 - webpStat.size / original) * 100).toFixed(0)}% smaller) | AVIF ${(avifStat.size / 1024).toFixed(0)}KB (${((1 - avifStat.size / original) * 100).toFixed(0)}% smaller)`)
}
}
optimizeImages('./public/images')4. 响应式图片
4.1 sizes 属性
sizes 告诉浏览器在不同视口宽度下图片的实际显示宽度。浏览器根据这个信息选择最合适的尺寸下载——不用在手机上下载桌面尺寸的图片。
如果不设 sizes,next/image 会生成一个保守的默认 srcset,可能导致手机下载过大的图片。
// 示例:桌面端图片占页面 33%,平板占 50%,手机占 100%
<Image
src={photo}
alt="Responsive photo"
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>sizes 的工作原理:浏览器在下载图片之前就知道了图片的显示宽度(通过 sizes),然后从 srcset 中选择最接近的尺寸。这发生在 CSS 解析之前,所以不能依赖 CSS 来决定图片尺寸。
4.2 常见响应式布局
// 全宽 Hero 图片
<Image
src={heroBanner}
alt="Hero"
fill
priority
sizes="100vw"
className="object-cover"
/>
// 网格布局(桌面 3 列,平板 2 列,手机 1 列)
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{photos.map((photo) => (
<div key={photo.id} className="relative aspect-[4/3]">
<Image
src={photo.url}
alt={photo.alt}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover rounded-lg"
/>
</div>
))}
</div>
// 侧边栏 + 内容区(侧边栏 300px,图片在内容区)
<Image
src={contentImage}
alt="Content"
fill
sizes="(max-width: 768px) 100vw, calc(100vw - 300px)"
/>4.3 Art Direction(不同设备不同图片)
有时不只是尺寸不同,而是需要在手机和桌面展示完全不同的图片(手机用竖版,桌面用横版)。这时需要 <picture> 元素——但 next/image 不直接支持。用 getImageProps 解决:
import { getImageProps } from 'next/image'
export function HeroBanner() {
// 桌面版横幅
const { props: desktopProps } = getImageProps({
src: '/images/hero-desktop.jpg',
alt: 'Hero banner',
width: 1920,
height: 600,
quality: 85,
})
// 移动版竖版图
const { props: mobileProps } = getImageProps({
src: '/images/hero-mobile.jpg',
alt: 'Hero banner',
width: 750,
height: 1000,
quality: 85,
})
return (
<picture>
<source media="(min-width: 768px)" srcSet={desktopProps.srcSet} />
<img {...mobileProps} className="w-full h-auto" />
</picture>
)
}5. 实战最佳实践
5.1 图片优化 Checklist
// ✅ 首屏 LCP 图片 — 加 priority
<Image src={hero} alt="Hero" fill priority sizes="100vw" />
// ✅ 本地图片 — 用 import + blur 占位
import photo from '@/public/images/photo.jpg'
<Image src={photo} alt="Photo" placeholder="blur" />
// ✅ 用户上传的图片 — 限制尺寸 + 配置远程域名
<Image
src={user.avatarUrl}
alt={user.name}
width={48}
height={48}
className="rounded-full"
/>
// ✅ 图标和装饰性图片 — 用 SVG,不用 next/image
import { SearchIcon } from 'lucide-react'
<SearchIcon className="w-5 h-5" />
// ❌ 不要给所有图片都加 priority
// ❌ 不要忽略 sizes 属性(尤其是 fill 模式)
// ❌ 不要用 next/image 处理 SVG(SVG 本身就是最优的)5.2 图片与字体联合优化效果
一个典型的优化前后对比:
| 指标 | 优化前 | 优化后 | 改善 |
|---|---|---|---|
| Hero 图片大小 | 2.4 MB (JPEG 4000×3000) | 85 KB (AVIF 800×600) | -96% |
| 字体文件大小 | 12 MB (完整中文字体) | 120 KB (分片加载) | -99% |
| LCP | 4.2s | 1.1s | -74% |
| CLS | 0.35 | 0.01 | -97% |
| 首屏总传输 | 15 MB | 800 KB | -95% |
本章小结
- next/image:自动格式转换(AVIF/WebP)、按需缩放、懒加载、blur 占位,用
priority标记 LCP 图片 - next/font:构建时下载字体、
size-adjust消除 CLS、自动子集化,推荐用 CSS 变量方式注入 - 格式选择:AVIF > WebP > JPEG,
next/image自动处理格式协商 - 响应式:
sizes属性告诉浏览器图片实际显示宽度,避免手机下载桌面尺寸图片 - 中文字体:优先用系统字体栈,其次用
next/font的自动分片,极致场景用fonttools子集化