Musa Yılmaz
·9 dk okuma

Vercel Görsel Optimizasyon Limitleri ve Kullanımı Düşürmek

vercelnextjsgörselperformans

Liste sayfasında on yazı duran bir blog ağır bir site gibi görünmez. Buna rağmen Vercel'in Hobby planında ilk tükenen sayaç genelde görsel optimizasyonu olur — bant genişliğinden de, fonksiyon çağrılarından da önce. Hobby ayda 5.000 görsel dönüşümü içeriyor ve küçük kapak görselleriyle dolu tek bir liste sayfası, ekranda hiçbir şey ters görünmeden yüzlerce olası önbellek girdisi açabiliyor.

Hobby limitlerinin kendisi Hobby planı yazısında duruyor. Bu yazı mekanizmayla ilgili: Vercel neyi sayıyor, tek bir görsel neden on beşe dönüşüyor ve next.config.ts içindeki hangi değerler bu sayıyı gerçekten düşürüyor. Aşağıdaki bütün varsayılanlar hafızadan değil, kurulu next@16.2.12 paketinin içinden okundu.

Tek sayaç değil, üç sayaç

Görsel optimizasyonu üç ayrı sayaç üzerinden faturalanıyor ve üçü farklı davranıyor:

SayaçHobby'de dahilNeyi tetikliyor
Görsel dönüşümü5.000 / ayHer önbellek MISS'i ve her STALE yenilemesi
Önbellek okuması300.000 / ayKüresel önbellekten bayt çekmek, 8 KB'lık birimlerle
Önbellek yazması100.000 / ayDönüştürülmüş baytı küresel önbelleğe yazmak, 8 KB'lık birimlerle

Sayılardan daha önemli iki ayrıntı var. Dönüşüm, kaynak görsel başına değil, önbellek ıskası başına faturalanıyor. Ve önbellek okuması her HIT'te faturalanmıyor: bir görsel aynı bölgede yakın zamanda istenmişse o bölgeden servis ediliyor ve bu sayaca hiç dokunmuyor.

Bunun pratik bir sonucu var. Kötü şekil, yazmanın okumaya yakın olduğu şekildir. Bir kez yazılıp bir kez okunan dönüşümler üretiyorsanız, kimsenin tekrar kullanmadığı varyantları üretmek için para ödüyorsunuz.

Bütçe, önbellek anahtarında

Vercel optimizasyon API'sinin önbellek anahtarını belgeliyor ve buradaki en işe yarar bilgi bu. Yerel bir görsel için anahtar şunlardan oluşuyor:

  • proje kimliği
  • q — istenen kalite
  • w — istenen piksel genişliği
  • url — yerel görsellerde dosyanın içerik özeti, uzak görsellerde tam URL
  • normalize edilmiş Accept istek başlığı

Yani dönüşüm bütçeniz yayına aldığınız görsel sayısı değil. Tarayıcıların fiilen istediği farklı (görsel, genişlik, kalite, format) kombinasyonlarının sayısı. Vercel kendi rehberinde bunu açıkça örnekliyor: 2 format, 2 kalite ve 8 genişlikte istenen tek bir hero görseli 32 önbellek girdisi demek.

Bu listede sayfa sayısı yok. Dolayısıyla çözüm hiçbir zaman "daha az görsel kullan" değil — "daha küçük bir varyant uzayı üret".

Tek görsel aslında kaç varyant istiyor

Genişlik uzayını sizin yerinize next/image belirliyor, ürettiği srcset üzerinden. Bu uzayın ne kadar geniş olduğunu tahmin etmeniz gerekmiyor, çünkü getImageProps herkese açık bir API ve çıktısını yazdırabiliyorsunuz. Aşağıdakini proje kökünde next@16.2.12 üzerinde çalıştırdım:

import { getImageProps } from 'next/image.js'
 
function say(etiket, opts) {
  const { props } = getImageProps(opts)
  const urls = props.srcSet.split(',').map((s) => s.trim())
  console.log(etiket, '->', urls.length, 'girdi')
  console.log(urls.map((u) => u.match(/[?&]w=(\d+)/)[1]).join(', '))
}
 
say('kapak, sizes 100vw sonra 768px', {
  src: '/assets/cover.png', alt: '', width: 1200, height: 630,
  sizes: '(max-width: 768px) 100vw, 768px',
})
 
say('kapak, sizes yok', {
  src: '/assets/cover.png', alt: '', width: 1200, height: 630,
})
 
say('128px küçük görsel', {
  src: '/assets/cover.png', alt: '', fill: true, sizes: '128px',
})

Import'taki .js uzantısı yalnızca bu betik paketleyicinin dışında, düz bir Node ESM betiği olarak çalıştığı için gerekiyor. Çıktı:

kapak, sizes 100vw sonra 768px -> 8 girdi
640, 750, 828, 1080, 1200, 1920, 2048, 3840
 
kapak, sizes yok -> 2 girdi
1200, 3840
 
128px küçük görsel -> 15 girdi
32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840

Üçüncü satırı bir daha okuyun. Hiçbir zaman 128 CSS pikselinden geniş gösterilmeyen bir küçük görsel, tarayıcıya 3840'a kadar uzanan on beş genişlik sunuyor. Bu on beş, iki varsayılan dizinin toplamının tam olarak kendisi: imageSizes 7, deviceSizes 8 girdi taşıyor.

Birinci durum da daha sessiz biçimde neredeyse aynı derecede savurgan. Görsel kendi sizes değeriyle 768 pikselde sınırlanmış, ama 1920, 2048 ve 3840 hâlâ menüde duruyor.

Maliyet konusunda net olalım: on beş URL için fatura kesilmiyor. Tarayıcı bir tanesini seçiyor. Ama o liste kaç farklı w değerinin erişilebilir olduğunu tanımlıyor ve yeterince farklı cihaz, piksel oranı ve tarayıcı botu geçtiğinde bunların iyi bir kısmına gerçekten erişiliyor — her biri ayrı bir önbellek anahtarı, her birinin ilk isteği bir dönüşüm.

deviceSizes ve imageSizes'ı yerleşimine göre kırp

16.2.12'deki varsayılanlar şunlar:

// next.config.ts — varsayılanlar, referans olsun diye
images: {
  deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
  imageSizes: [32, 48, 64, 96, 128, 256, 384],
}

deviceSizes, görüntü alanının bir oranını kaplayan görseller için kullanılıyor. imageSizes ise yalnızca sizes özelliği verilmiş görseller için devreye giriyor ve içindeki her değer deviceSizes'ın en küçük değerinden küçük olmalı.

Okuma kolonu 768 piksel civarında olan bir içerik sitesinin hiçbir şeyin 3840 piksellik sürümüne ihtiyacı yok; 128 piksellik bir avatarın da yedi aday genişliğe. Dizileri yerleşiminizin gerçekten üretebileceği genişliklere göre ayarlayın, 2x payını da hesaba katarak:

images: {
  deviceSizes: [640, 828, 1080, 1536],
  imageSizes: [128, 256],
}

Bu, 15 yerine 6 erişilebilir genişlik demek. Tasarruf bu orandan geliyor ve yalnız dönüşümlere değil önbellek yazmalarına da yansıyor.

Bir uyarı: bu dizileri fazla kısarsanız bir cihaz kendi alanından dar bir görsel alır ve onu büyütür. En büyük değeri, en geniş gösterdiğiniz görselin kabaca iki katında tutun.

Tek formatta kal

Varsayılan tek format:

images: { formats: ['image/webp'] }

AVIF eklemek saf kazanç gibi duruyor — Next.js onu WebP'ye göre yaklaşık %20 daha küçük diye belgeliyor — ama kodlaması %50 daha uzun sürüyor ve burada daha önemlisi, Next.js her formatı ayrı önbellekliyor. İki format, iki farklı normalize Accept değeri, yani genişlik uzayının ikiyle çarpılması demek. 5.000 dönüşümlü bir planda, baytlardan %20 indirim için kardinaliteyi ikiye katlamak çoğunlukla yanlış takas. WebP'de bırakın.

minimumCacheTTL'i yükselt, varsayılana güvenme

Dokümantasyonun kendisiyle çeliştiği yer burası, o yüzden varsaymak yerine kontrol edin. Vercel'in görsel optimizasyon sayfası uzak görseller için varsayılan TTL'i 3600 saniye diyor. Kurulu paketteki varsayılan bunun dört katı:

// node_modules/next/dist/shared/lib/image-config.js
minimumCacheTTL: 14400,

Her iki durumda da bir blog kapağı dört saatte bir değişmiyor. Görselleriniz fiilen değişmiyorsa bunu söyleyin:

images: { minimumCacheTTL: 2678400 } // 31 gün

Daha az sona erme, daha az STALE yenilemesi; önlenen her yenileme harcamadığınız bir dönüşüm. Geçerli TTL, bu değer ile kaynak görselin Cache-Control max-age'i arasında büyük olanı oluyor. Statik import'lar daha da iyisini yapıyor: dosya içeriğini özetleyip değişmez olarak önbellekleniyorlar.

Ama bedeli gerçek ve Next.js bunu doğrudan yazıyor: bu önbelleği geçersiz kılmanın bir yolu yok. Yeniden dağıtım temizlemiyor. Vercel'de CDN önbelleğini elle ya da programatik olarak purge edebiliyorsunuz; kendi sunucunuzda ya src'yi değiştiriyor ya da <distDir>/cache/images dizinini siliyorsunuz. Yani 31 günlük TTL içerik görselleri için doğru, kullanıcının yerinde değiştirebildiği hiçbir şey için doğru değil.

qualities listesini kilitle

Next.js 16'da qualities zorunlu bir izin listesine dönüştü ve dokümanda verilen gerekçe tam olarak faturayı ilgilendiren gerekçe: liste olmadan herkes rastgele kalite değerleri isteyip sizin hiç niyet etmediğiniz dönüşümleri üretebiliyor. Varsayılan tek değer:

images: { qualities: [75] }

Bir ya da iki değerde tutun. quality özelliği listede olmayan bir istek en yakın izinli değere yuvarlanıyor; optimizasyon uç noktasına izinli olmayan bir kaliteyle doğrudan gidilirse 400 dönüyor. Tek izinli kalite, kardinalite tablosunda yüz satır yerine bir satır demek.

Pattern'leri kapat, search dahil

remotePatterns ve localPatterns genelde erişim denetimi sayılıyor, ama aynı zamanda harcama denetimi — hangi URL'lerin dönüşüme dönüşmeye hakkı olduğuna onlar karar veriyor. Tuzak search alanında:

images: {
  remotePatterns: [
    {
      protocol: 'https',
      hostname: 'images.example.com',
      pathname: '/account123/**',
      search: '',
    },
  ],
}

search'ü yazmazsanız her sorgu dizesi geçerli olur; yani aynı görsel yüz farklı önbellek kırıcı sonekle yüz farklı önbellek anahtarı demektir. search: '' hiç sorgu dizesi olmamasını şart koşuyor. Next.js ayrıca hostname sizin değilse hesap ya da bucket bölümünü pathname içine koymanızı öneriyor — böylece paylaşımlı bir host üzerinden sizin projenize dönüşüm faturalanamıyor.

Hiçbir şey kazandırmayan yerde optimizasyonu atla

Bazı görseller bu hattan hiçbir şey kazanmıyor: SVG'ler, hareketli GIF'ler ve 10 KB'ın altındaki her şey. Bunları optimize etmek, anlamlı biçimde küçülmeyen bir dosya üretmek için bir dönüşüm, bir önbellek yazması ve bir önbellek okuması harcıyor.

<Image src="/assets/logo.svg" alt="" width={120} height={32} unoptimized />

Bunu görsel bazında kullanın, global olarak değil. Yapılandırmada unoptimized: true demek tüm uygulamayı orijinalleri servis etmeye çeviriyor; dönüşüm faturanızı Fast Data Transfer faturasıyla takas ediyorsunuz ve Hobby'de o tavan 100 GB.

Gerçekten tükendiğinde ne oluyor

Panikle ayar yapmaya başlamadan önce bilmeye değer: dahil edilen görsel optimizasyon kullanımını aşmak dağıtımınızı durdurmuyor. Vercel yalnızca yeni kaynak görseller için optimizasyonu durduruyor. Önbellekte olan görseller servis edilmeye devam ediyor, diğer trafik etkilenmiyor. Yeni kaynak görseller optimizasyon uç noktasından 402 alıyor; bu, tanımladıysanız onError geri çağrısını tetikliyor ve görselin yerine alt metnini gösteriyor.

Bu, gerçek alt metni yazmak için iyi bir gerekçe — ve tanınması gereken arıza biçimi de bu: bazı görseller yerinde, yeni eklenenlerin yerinde yazı.

Kendi sunucunda aritmetik değişiyor

Vercel dışında bu sayaçların hiçbiri yok — bunun yerine CPU ve diskle ödüyorsunuz, ve disk tarafının kendi ayarı var:

images: { maximumDiskCacheSize: 500_000_000 } // 500 MB

Değer verilmezse Next.js açılışta bir kez boş disk alanını ölçüp yarısını kullanıyor. Önbellek sınırı geçtiğinde en az kullanılan girdiler siliniyor. Vercel'den çıkınca değişen diğer her şey kendi sunucunda çalıştırma yazısında — görsel optimizasyonunun kendisi yalnızca sharp paketini istiyor.

Özet

Bütçe kardinalite, görsel sayısı değil. Etki sırasına göre:

  1. deviceSizes ve imageSizes'ı yerleşiminizin gerçekten üretebileceği genişliklere kırpın. Büyük kazanç burada — varsayılan yapılandırma 128 piksellik bir küçük görseli 15 genişlikte erişilebilir yapıyor.
  2. formats'ı tek girdide tutun. İki format her şeyi ikiye katlıyor.
  3. minimumCacheTTL'i açıkça yazın. Belgelenen varsayılan ile paketteki varsayılan uyuşmuyor.
  4. qualities'i tek değerde tutun.
  5. Pattern'lerde search: '' verin, sorgu dizeleri önbellek anahtarlarınızı çoğaltamasın.
  6. SVG, GIF ve çok küçük görselleri görsel bazında unoptimized işaretleyin.

Sonra toplama değil şekle bakın: önbellek okumaları yazmalardan hızlı büyümeli. Yazmalar okumalara ayak uyduruyorsa hâlâ kimsenin iki kez istemediği varyantlar üretiyorsunuz. Bu arada sizes üzerinde oynuyorsanız neyi daha etkilediğini bilmek işe yarar — eksik ya da yanlış sizes, aynı zamanda yerleşim kaymasının yaygın sebeplerinden biri.