perf: add AVIF/WebP next image config and image optimization script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-11 13:04:04 +01:00
parent a0cde76789
commit 63af1f86e3
3 changed files with 130 additions and 11 deletions

View file

@ -26,18 +26,13 @@ const nextConfig: NextConfig = {
return config
},
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [375, 640, 768, 1024, 1280, 1536, 1920],
imageSizes: [16, 32, 64, 96, 128, 256, 384, 512, 694],
minimumCacheTTL: 31536000,
remotePatterns: [
{
protocol: 'http',
hostname: 'localhost',
port: '3000',
pathname: '/media/**',
},
{
protocol: 'https',
hostname: 'shumiland.com.ua',
pathname: '/media/**',
},
{ protocol: 'http', hostname: 'localhost', port: '3000', pathname: '/media/**' },
{ protocol: 'https', hostname: 'shumiland.com.ua', pathname: '/media/**' },
],
},
}

View file

@ -0,0 +1,75 @@
{
"deletedDuplicates": [
"081e52b5-d35a-41d2-b506-a9d751b0b563",
"109f13b4-25f9-4f94-a06d-77421ff2b4fe",
"17d78753-cec3-40ca-a1fc-4125c2b79eff",
"2936ec5e-4f99-441e-9bf2-34f23c283170",
"2c6a3e5e-7346-4c3e-b8a0-fae1facb87ad",
"455670b9-c89a-4bea-81fe-5a4d93b25483",
"5088f41f-b898-4958-b7f6-519496b65382",
"58218f84-457d-4ff5-8d4e-2ace40b45568",
"640999e1-7096-4623-bf96-12bb8ef62ffc",
"7a2627b2-b6ce-4325-a0b1-fbc3393aca4c",
"908a8aab-6129-4d7d-b7ac-c43b6fa60044",
"abacda18-57c7-441d-9313-c70d22c6f0f0",
"ac7f3a5e-0e66-4971-bccc-89901a7c314d",
"b5597790-7b54-4ad7-9977-4cb15633286e",
"c3053789-6cd0-4dda-9774-d4ae4bc400e1",
"de9ad287-5e83-4a0c-ad06-f2420bff4096",
"e42d6611-82e6-47b0-9d84-271f9810ab1a",
"e9a8cee6-6ee5-4c74-b270-1c133a762c0a",
"f4e2bff2-754c-460c-baba-baa95521bfc9",
"f5d32fc0-8ea7-4fd1-b193-819d6aa1a68e"
],
"encoded": [],
"skipped": [
"081e52b5-d35a-41d2-b506-a9d751b0b563.png (webp newer)",
"109f13b4-25f9-4f94-a06d-77421ff2b4fe.jpg (webp newer)",
"17d78753-cec3-40ca-a1fc-4125c2b79eff.png (webp newer)",
"2936ec5e-4f99-441e-9bf2-34f23c283170.jpg (webp newer)",
"2c6a3e5e-7346-4c3e-b8a0-fae1facb87ad.jpg (webp newer)",
"455670b9-c89a-4bea-81fe-5a4d93b25483.png (webp newer)",
"5088f41f-b898-4958-b7f6-519496b65382.png",
"58218f84-457d-4ff5-8d4e-2ace40b45568.png (webp newer)",
"640999e1-7096-4623-bf96-12bb8ef62ffc.png (webp newer)",
"7a2627b2-b6ce-4325-a0b1-fbc3393aca4c.png (webp newer)",
"908a8aab-6129-4d7d-b7ac-c43b6fa60044.png (webp newer)",
"abacda18-57c7-441d-9313-c70d22c6f0f0.png (webp newer)",
"ac7f3a5e-0e66-4971-bccc-89901a7c314d.png (webp newer)",
"b5597790-7b54-4ad7-9977-4cb15633286e.png",
"c3053789-6cd0-4dda-9774-d4ae4bc400e1.png (webp newer)",
"check-mark.png",
"de9ad287-5e83-4a0c-ad06-f2420bff4096.png",
"e42d6611-82e6-47b0-9d84-271f9810ab1a.png (webp newer)",
"e9a8cee6-6ee5-4c74-b270-1c133a762c0a.png (webp newer)",
"f4e2bff2-754c-460c-baba-baa95521bfc9.png (webp newer)",
"f5d32fc0-8ea7-4fd1-b193-819d6aa1a68e.png (webp newer)",
"footer-bg.png",
"gallery-1.png",
"gallery-2.png (webp newer)",
"gallery-3.png",
"gallery-4.png (webp newer)",
"gallery-6.png (webp newer)",
"gallery-7.png (webp newer)",
"gallery-8.png (webp newer)",
"hero-bg-family.png (webp newer)",
"hero-bg1.png (webp newer)",
"hero-bg2.png (webp newer)",
"hero-blur-mask.png",
"loc-dinopark.jpg (webp newer)",
"loc-divo-lis.png (webp newer)",
"loc-map.jpg (webp newer)",
"news-bg1.jpg",
"news-bg2.png (webp newer)",
"news-bg3.jpg (webp newer)",
"news-bg4.png (webp newer)",
"news-bg5.png (webp newer)",
"news-bg6.png (webp newer)",
"review-avatar-bg.jpg (webp newer)",
"video-preview.png (webp newer)",
"why-parents-1.png (webp newer)",
"why-parents-2.png (webp newer)",
"why-parents-3.png (webp newer)",
"why-parents-4.png (webp newer)"
]
}

View file

@ -0,0 +1,49 @@
#!/usr/bin/env node
import fs from 'node:fs/promises'
import path from 'node:path'
import sharp from 'sharp'
const ROOT = path.resolve(process.cwd(), 'public/images/figma')
const QUALITY = 82
const MIN_BYTES = 200_000
const manifest = { deletedDuplicates: [], encoded: [], skipped: [] }
const all = await fs.readdir(ROOT)
const byBase = new Map()
for (const f of all) {
const ext = path.extname(f)
const base = ext ? path.basename(f, ext) : f
if (!byBase.has(base)) byBase.set(base, [])
byBase.get(base).push(f)
}
// 1) Delete extensionless duplicates that have a webp/png/jpg sibling
for (const [, variants] of byBase) {
const extensionless = variants.find((v) => path.extname(v) === '')
const hasSibling = variants.some((v) => /\.(webp|png|jpg|jpeg)$/i.test(v))
if (extensionless && hasSibling) {
const p = path.join(ROOT, extensionless)
await fs.unlink(p)
manifest.deletedDuplicates.push(path.basename(p))
}
}
// 2) Re-encode large png/jpg to webp (skip if webp already exists and is newer)
for (const [base, variants] of byBase) {
const heavy = variants.find((v) => /\.(png|jpg|jpeg)$/i.test(v))
if (!heavy) continue
const srcPath = path.join(ROOT, heavy)
const stat = await fs.stat(srcPath).catch(() => null)
if (!stat || stat.size < MIN_BYTES) { manifest.skipped.push(path.basename(srcPath)); continue }
const outPath = path.join(ROOT, `${base}.webp`)
const outStat = await fs.stat(outPath).catch(() => null)
if (outStat && outStat.mtimeMs > stat.mtimeMs) { manifest.skipped.push(path.basename(srcPath) + ' (webp newer)'); continue }
await sharp(srcPath).webp({ quality: QUALITY }).toFile(outPath)
manifest.encoded.push({ src: path.basename(srcPath), out: `${base}.webp`, before: stat.size })
}
const manifestPath = path.join(process.cwd(), 'scripts/optimize-images.manifest.json')
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2))
console.log(`Deleted ${manifest.deletedDuplicates.length} duplicates, encoded ${manifest.encoded.length} files, skipped ${manifest.skipped.length}.`)
console.log('Manifest:', manifestPath)