🙋 图片是网页加载的大头。Next.js Image 组件能自动优化图片,大幅提升性能。
为什么使用 next/image#
| 特性 | 原生 img | next/image |
|---|---|---|
| 自动格式转换 | ❌ | ✅ WebP/AVIF |
| 响应式尺寸 | 手动 | 自动 |
| 懒加载 | 手动 | 默认开启 |
| 尺寸优化 | ❌ | ✅ |
| 布局稳定性 | ❌ | ✅ 避免 CLS |
基础用法#
本地图片#
import Image from 'next/image'import profilePic from './profile.jpg'
export default function Page() { return ( <Image src={profilePic} alt="个人头像" // 自动推断宽高 placeholder="blur" // 模糊占位 /> )}本地图片自动获得:
- 宽高推断(避免布局偏移)
- 模糊占位(blur placeholder)
远程图片#
import Image from 'next/image'
export default function Page() { return ( <Image src="https://example.com/photo.jpg" alt="远程图片" width={800} height={600} /> )}🔶 注意:远程图片必须指定 width 和 height,或使用 fill 属性。
配置远程图片域名#
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..."/>生成模糊数据#
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}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}/>全局配置:
const config: NextConfig = { images: { quality: 80, // 全局默认质量 },}图片格式#
Next.js 自动选择最佳格式:
const config: NextConfig = { images: { formats: ['image/avif', 'image/webp'], // 按顺序尝试,回退到原格式 },}常见模式#
头像组件#
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" /> )}图片画廊#
'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> )}背景图片#
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> )}产品卡片#
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> )}性能优化配置#
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#
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}`}const config: NextConfig = { images: { loader: 'custom', loaderFile: './lib/cloudinary-loader.ts', },}使用 Imgix#
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-