

<picture>
<source type="image/avif" srcset="/img/hero-640.avif 640w, /img/hero-1200.avif 1200w" />
<source type="image/webp" srcset="/img/hero-640.webp 640w, /img/hero-1200.webp 1200w" />
<img
src="/img/hero-1200.jpg"
alt="hero"
width="1200"
height="800"
loading="eager"
fetchpriority="high"
decoding="async"
srcset="/img/hero-640.jpg 640w, /img/hero-1200.jpg 1200w"
sizes="(max-width: 640px) 640px, 1200px"
/>
</picture>loading="eager" + fetchpriority="high";其他图像 loading="lazy"width/height)或 CSS aspect-ratio,避免 CLS<img loading="lazy"> 适合非关键图像const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
const img = e.target
img.src = img.dataset.src
img.srcset = img.dataset.srcset || ''
io.unobserve(img)
}
})
})
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img))Accept 返回 AVIF 或 WebP,未支持时返回 JPEG/PNGmap $http_accept $img_format {
default jpg;
~*avif avif;
~*webp webp;
}
server {
location ~* ^/img/(.*)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary "Accept";
try_files /img/$1.$img_format /img/$1.jpg =404;
}
}preload as=image;其余用 prefetch/img/hero.jpg?format=avif&width=640&quality=65 由边缘进行转换与裁剪import fs from 'node:fs'
import sharp from 'sharp'
async function convert(input, base) {
const img = sharp(input)
await img.resize(1200).avif({ quality: 65 }).toFile(`${base}-1200.avif`)
await img.resize(640).avif({ quality: 65 }).toFile(`${base}-640.avif`)
await img.resize(1200).webp({ quality: 75 }).toFile(`${base}-1200.webp`)
await img.resize(640).webp({ quality: 75 }).toFile(`${base}-640.webp`)
}
for (const f of fs.readdirSync('src/img')) {
const base = `public/img/${f.replace(/\.(png|jpe?g)$/,'')}`
await convert(`src/img/${f}`, base)
}vite-imagetools 或 Rollup image-plugin 管理派生资源与 srcsetfont-display: swap;图标优先 SVG Sprite/组件,避免位图图标eager + fetchpriority="high"width/height 或 aspect-ratiopreload:仅关键图像使用;其他用 prefetchpreload 与高优先级;其余懒加载Vary: Accept 与 try_files 回退immutable 静态资源;合理 TTL 与 Keypreconnect CDN 域名