Skip to content

图片优化

🙋 图片是网页加载的大头。Next.js Image 组件能自动优化图片,大幅提升性能。

为什么使用 next/image#

特性原生 imgnext/image
自动格式转换✅ WebP/AVIF
响应式尺寸手动自动
懒加载手动默认开启
尺寸优化
布局稳定性✅ 避免 CLS

基础用法#

本地图片#

app/page.tsx
import Image from 'next/image'
import profilePic from './profile.jpg'
export default function Page() {
return (
<Image
src={profilePic}
alt="个人头像"
// 自动推断宽高
placeholder="blur" // 模糊占位
/>
)
}

本地图片自动获得:

远程图片#

app/page.tsx
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://example.com/photo.jpg"
alt="远程图片"
width={800}
height={600}
/>
)
}

🔶 注意:远程图片必须指定 widthheight,或使用 fill 属性。

配置远程图片域名#

next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
port: '',
pathname: '/images/**',
},
{
protocol: 'https',
hostname: '**.cloudinary.com',
},
],
},
}
export default config

布局模式#

固定尺寸#

<Image src="/photo.jpg" alt="固定尺寸" width={400} height={300} />

填充容器 (fill)#

// 图片填充父容器
<div className="relative w-full h-64">
<Image src="/photo.jpg" alt="填充容器" fill className="object-cover" />
</div>

响应式#

<Image
src="/photo.jpg"
alt="响应式"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
fill
className="object-cover"
/>

sizes 告诉浏览器在不同视口宽度下图片的实际显示宽度。

占位符#

模糊占位#

// 本地图片自动支持
import Image from 'next/image'
import photo from './photo.jpg'
<Image
src={photo}
alt="模糊占位"
placeholder="blur"
/>
// 远程图片需要提供 blurDataURL
<Image
src="https://example.com/photo.jpg"
alt="远程图片"
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>

生成模糊数据#

lib/image.ts
import { getPlaiceholder } from 'plaiceholder'
export async function getBlurDataURL(src: string) {
const buffer = await fetch(src).then((res) => res.arrayBuffer())
const { base64 } = await getPlaiceholder(Buffer.from(buffer))
return base64
}
app/photos/page.tsx
import Image from 'next/image'
import { getBlurDataURL } from '@/lib/image'
export default async function PhotosPage() {
const photos = [
{ url: 'https://example.com/photo1.jpg', alt: '照片1' },
{ url: 'https://example.com/photo2.jpg', alt: '照片2' },
]
const photosWithBlur = await Promise.all(
photos.map(async (photo) => ({
...photo,
blurDataURL: await getBlurDataURL(photo.url),
}))
)
return (
<div className="grid grid-cols-2 gap-4">
{photosWithBlur.map((photo) => (
<div key={photo.url} className="relative aspect-video">
<Image
src={photo.url}
alt={photo.alt}
fill
placeholder="blur"
blurDataURL={photo.blurDataURL}
/>
</div>
))}
</div>
)
}

颜色占位#

<Image
src="https://example.com/photo.jpg"
alt="颜色占位"
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNlNWU1ZTUiLz48L3N2Zz4="
/>

加载优先级#

首屏图片优先加载#

<Image
src="/hero.jpg"
alt="首屏大图"
width={1920}
height={1080}
priority // 预加载,禁用懒加载
/>

懒加载(默认行为)#

<Image
src="/photo.jpg"
alt="懒加载图片"
width={800}
height={600}
loading="lazy" // 默认值
/>

响应式图片#

srcset 自动生成#

<Image
src="/photo.jpg"
alt="响应式"
width={800}
height={600}
// 自动生成多个尺寸
/>

生成的 HTML 类似:

<img
srcset="
/_next/image?url=%2Fphoto.jpg&w=640&q=75 640w,
/_next/image?url=%2Fphoto.jpg&w=750&q=75 750w,
/_next/image?url=%2Fphoto.jpg&w=828&q=75 828w,
...
"
sizes="100vw"
/>

自定义 sizes#

// 响应式网格布局
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{images.map((image) => (
<div key={image.id} className="relative aspect-square">
<Image
src={image.url}
alt={image.alt}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
</div>
))}
</div>

图片质量#

// 默认质量 75
<Image
src="/photo.jpg"
alt="高质量"
width={800}
height={600}
quality={90}
/>
// 低质量用于缩略图
<Image
src="/thumbnail.jpg"
alt="缩略图"
width={200}
height={200}
quality={50}
/>

全局配置:

next.config.ts
const config: NextConfig = {
images: {
quality: 80, // 全局默认质量
},
}

图片格式#

Next.js 自动选择最佳格式:

next.config.ts
const config: NextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
// 按顺序尝试,回退到原格式
},
}

常见模式#

头像组件#

components/Avatar.tsx
import Image from 'next/image'
interface AvatarProps {
src: string
alt: string
size?: 'sm' | 'md' | 'lg'
}
const sizes = {
sm: 32,
md: 48,
lg: 64,
}
export function Avatar({ src, alt, size = 'md' }: AvatarProps) {
const dimension = sizes[size]
return (
<Image
src={src}
alt={alt}
width={dimension}
height={dimension}
className="rounded-full object-cover"
/>
)
}

图片画廊#

components/Gallery.tsx
'use client'
import Image from 'next/image'
import { useState } from 'react'
interface GalleryProps {
images: { src: string; alt: string }[]
}
export function Gallery({ images }: GalleryProps) {
const [selectedIndex, setSelectedIndex] = useState(0)
return (
<div className="space-y-4">
{/* 主图 */}
<div className="relative aspect-video">
<Image
src={images[selectedIndex].src}
alt={images[selectedIndex].alt}
fill
className="object-contain"
priority
/>
</div>
{/* 缩略图 */}
<div className="grid grid-cols-5 gap-2">
{images.map((image, index) => (
<button
key={image.src}
onClick={() => setSelectedIndex(index)}
className={`relative aspect-square ${
index === selectedIndex ? 'ring-2 ring-blue-600' : ''
}`}
>
<Image
src={image.src}
alt={image.alt}
fill
className="object-cover"
sizes="20vw"
/>
</button>
))}
</div>
</div>
)
}

背景图片#

components/Hero.tsx
import Image from 'next/image'
export function Hero() {
return (
<section className="relative h-screen">
<Image
src="/hero-bg.jpg"
alt=""
fill
className="object-cover"
priority
quality={85}
/>
<div className="absolute inset-0 bg-black/50" />
<div className="relative z-10 flex items-center justify-center h-full">
<h1 className="text-5xl font-bold text-white">欢迎</h1>
</div>
</section>
)
}

产品卡片#

components/ProductCard.tsx
import Image from 'next/image'
import Link from 'next/link'
interface ProductCardProps {
id: string
name: string
price: number
image: string
}
export function ProductCard({ id, name, price, image }: ProductCardProps) {
return (
<Link href={`/products/${id}`} className="group">
<div className="relative aspect-square overflow-hidden rounded-lg bg-gray-100">
<Image
src={image}
alt={name}
fill
className="object-cover transition-transform group-hover:scale-105"
sizes="(max-width: 768px) 50vw, (max-width: 1200px) 33vw, 25vw"
/>
</div>
<h3 className="mt-2 font-medium">{name}</h3>
<p className="text-gray-600">¥{price}</p>
</Link>
)
}

性能优化配置#

next.config.ts
const config: NextConfig = {
images: {
// 设备尺寸断点
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
// 图片尺寸断点
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// 格式优先级
formats: ['image/avif', 'image/webp'],
// 最小缓存时间(秒)
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 天
// 禁用静态导入
disableStaticImages: false,
// 自定义 loader
loader: 'default',
},
}

自定义 Loader#

使用 Cloudinary#

lib/cloudinary-loader.ts
export default function cloudinaryLoader({
src,
width,
quality,
}: {
src: string
width: number
quality?: number
}) {
const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`]
return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`
}
next.config.ts
const config: NextConfig = {
images: {
loader: 'custom',
loaderFile: './lib/cloudinary-loader.ts',
},
}

使用 Imgix#

lib/imgix-loader.ts
export default function imgixLoader({
src,
width,
quality,
}: {
src: string
width: number
quality?: number
}) {
const url = new URL(`https://example.imgix.net${src}`)
url.searchParams.set('auto', 'format')
url.searchParams.set('fit', 'max')
url.searchParams.set('w', width.toString())
if (quality) {
url.searchParams.set('q', quality.toString())
}
return url.href
}

常见问题#

🤔 Q: 远程图片报错怎么办?

检查 next.config.ts 是否配置了 remotePatterns

images: {
remotePatterns: [
{ hostname: 'example.com' },
],
}

🤔 Q: 如何禁用图片优化?

<Image src="/photo.jpg" alt="未优化" width={800} height={600} unoptimized />

或全局禁用:

images: {
unoptimized: true,
}

🤔 Q: fill 模式图片变形怎么办?

使用 object-fit

<Image
src="/photo.jpg"
alt="保持比例"
fill
className="object-cover" // 或 object-contain
/>

下一篇将介绍字体优化,学习如何使用 next/font 优化字体加载。

-EOF-