Apa yang Sebenarnya Dilakukan next/image: srcset, AVIF, dan Cache

Foto oleh TheRegisti

Foto oleh TheRegisti
Kebanyakan developer Next.js memperlakukan komponen Image sebagai kotak ajaib: masukkan gambar, keluar performa. Lalu kotaknya berulah — gambar hero terkirim selebar 3840 piksel ke ponsel, disk server penuh file misterius di bawah direktori build, atau gambar basi menolak diperbarui setelah deploy — dan tiba-tiba tidak ada yang tahu harus melihat ke mana, karena tidak ada yang tahu apa yang sebenarnya dilakukan kotak itu.
Saya menjalankan beberapa situs Next.js di infrastruktur yang saya kelola sendiri, artinya saat image optimizer berulah, sayalah tim CDN-nya. Artikel ini membuka kotaknya: HTML persis yang dirender next/image, cara kerja negosiasi format antara AVIF dan WebP, di mana cache optimizer hidup dan bagaimana ia melakukan eviksi, serta empat pola penyalahgunaan yang terus saya temukan di code review yang diam-diam membuang seluruh pipeline.
Tidak ada mekanisme pemuatan proprietary. Komponen ini merender elemen img standar dan mendelegasikan logika pemilihan ke browser lewat srcset dan sizes — primitif responsive image yang sama yang sudah didokumentasikan MDN selama satu dekade. Yang ditambahkan Next.js adalah pembangkitan: setiap URL di srcset menunjuk ke endpoint optimasi di bawah /_next/image, dengan width dan quality tertanam di query string. Lebar kandidatnya datang langsung dari dua array konfigurasi: deviceSizes, default 640 sampai 3840, dan imageSizes, default 32 sampai 384, untuk gambar yang lebih kecil dari viewport.
// What you write:
<Image src={hero} alt="Dashboard" sizes="(max-width: 768px) 100vw, 50vw" />
// Roughly what the browser receives:
<img
src="/_next/image?url=%2Fhero.jpg&w=3840&q=75"
srcset="
/_next/image?url=%2Fhero.jpg&w=640&q=75 640w,
/_next/image?url=%2Fhero.jpg&w=750&q=75 750w,
/_next/image?url=%2Fhero.jpg&w=828&q=75 828w,
/_next/image?url=%2Fhero.jpg&w=1080&q=75 1080w,
/_next/image?url=%2Fhero.jpg&w=1200&q=75 1200w,
/_next/image?url=%2Fhero.jpg&w=1920&q=75 1920w,
/_next/image?url=%2Fhero.jpg&w=2048&q=75 2048w,
/_next/image?url=%2Fhero.jpg&w=3840&q=75 3840w"
sizes="(max-width: 768px) 100vw, 50vw"
loading="lazy" decoding="async"
width="..." height="..." // reserved space = no layout shift
/>
// Without a sizes prop you get only a 1x/2x srcset —
// fine for fixed-width images, wasteful for responsive ones.Prop sizes adalah engsel seluruh sistem. Tanpanya, Next.js mengasumsikan gambar berukuran tetap dan mengeluarkan srcset minimal 1x dan 2x. Dengannya, Anda mendapat srcset deskriptor lebar penuh seperti di atas, dan browser memilih kandidat terkecil yang memenuhi layout — itulah persisnya bagaimana sebuah ponsel akhirnya mengunduh varian 640 piksel dari hero Anda alih-alih yang 3840. Perhatikan juga width dan height menjadi atribut sungguhan: kotak aspect-ratio yang dipesan itulah alasan next/image yang dipakai benar menghasilkan nol layout shift dari gambar.
Pemilihan format adalah content negotiation HTTP klasik, bukan sihir URL. Browser mengumumkan apa yang bisa ia decode lewat request header Accept, dan optimizer menjawab dengan format terbaik yang Anda aktifkan. Konfigurasi default menyajikan WebP; Anda memilih AVIF dengan mencantumkan kedua format di konfigurasi images, dan AVIF menang untuk browser yang mendukungnya sementara WebP tetap jadi fallback.
Haruskah Anda mengaktifkan AVIF? Angka resminya membingkai trade-off-nya: kompresi AVIF kira-kira dua puluh persen lebih kecil dari WebP tapi butuh sekitar lima puluh persen lebih lama untuk encode. Di deployment yang dilindungi CDN, biaya encode itu teramortisasi jadi nol. Di VPS self-hosted di mana optimizer berbagi core CPU dengan aplikasi Anda — situasi saya, dan situasi kebanyakan deployment UKM Indonesia yang pernah saya bangun — setiap gambar dingin kini membebani komputasi lebih, dan cache menyimpan tiap format terpisah, menggandakan pemakaian disk untuk set gambar yang sama. Saya mengirim WebP-saja di server kecil dan tidak pernah menyesal.
Karena negosiasi terjadi per request header, URL yang sama menyajikan byte berbeda ke browser berbeda. Jika Anda menaruh cache atau proxy yang salah konfigurasi di depan optimizer dan mengabaikan header Vary, Anda bisa menyajikan AVIF ke browser yang tidak bisa men-decode-nya. Kalau self-host di belakang nginx, pastikan proxy cache Anda menghormati Vary Accept.
Setiap varian yang dioptimasi — tiap width, tiap quality, tiap format — ditulis ke disk supaya request berikutnya menjadi pembacaan file alih-alih encode ulang. Mekanisme yang perlu diketahui:
Poin tanpa-invalidasi layak diulang, karena ia menghasilkan insiden klasik: tim marketing mengganti banner.jpg dengan file baru bernama sama, deploy keluar, dan gambar lama terus tersaji sampai TTL habis. Perbaikan yang kokoh adalah nama file ber-hash konten — yang diberikan static import secara gratis — sehingga gambar yang berubah, menurut definisi, adalah URL baru.
Keempatnya terus lolos code review, karena halamannya tetap terlihat baik. Waterfall-lah yang berkata jujur:
| Kesalahan | Biayanya | Perbaikan |
|---|---|---|
| fill atau layout responsif tanpa prop sizes | Browser mengasumsikan gambar selebar viewport pada 100vw, memilih kandidat raksasa, dan pengguna ponsel Anda mengunduh file berukuran desktop. | Tulis string sizes yang jujur sesuai layout CSS, misalnya 100vw di mobile dan 33vw di grid tiga kolom. |
| quality 90 ke atas di mana-mana | Ukuran file membengkak demi fidelitas yang tidak dirasakan siapa pun pada citra konten, dan tiap varian di-encode serta di-cache dengan biaya itu. | Default 75 sudah dipilih dengan baik. Batasi nilai yang diterima lewat konfigurasi qualities agar prop yang nyasar tidak meregresikannya. |
| Lazy-load pada gambar LCP | Default loading lazy menunda hero Anda sampai layout stabil — langsung menggelembungkan Largest Contentful Paint ratusan milidetik. | Set priority pada satu gambar above-the-fold per halaman; ia beralih ke eager loading dan mengeluarkan hint preload. |
| remotePatterns yang terbuka lebar | Optimizer Anda menjadi proxy resize gambar gratis bagi internet, membakar CPU dan disk Anda untuk URL sembarang. | Allowlist hostname persis yang benar-benar Anda pakai. Perlakukan endpoint optimizer sebagai sumber daya komputasi yang memang demikian adanya. |
Inilah konfigurasi images yang saya deploy di proyek-proyek ber-VPS, dengan alasannya tertulis inline:
// next.config.ts — my self-hosted production baseline
const nextConfig = {
images: {
// WebP only: AVIF encodes ~50% slower and doubles the
// disk cache (each format cached separately). Enable AVIF
// when a CDN absorbs the cost, skip it on a small VPS.
formats: ["image/webp"],
// Optimized output lives in <distDir>/cache/images.
// There is NO invalidation API — keep the TTL modest.
minimumCacheTTL: 14400, // 4 hours
maximumDiskCacheSize: 500_000_000, // LRU-evicted past 500 MB
// Never proxy arbitrary URLs through your optimizer:
remotePatterns: [
{ protocol: "https", hostname: "images.unsplash.com" },
],
// Restrict the quality values the API accepts (Next 15+):
qualities: [60, 75],
},
}
export default nextConfignext/image bukan sihir — ia tiga mekanisme jujur yang disatukan: pembangkitan srcset terhadap array lebar yang Anda konfigurasi, negosiasi format lewat header Accept, dan cache disk dengan eviksi LRU tanpa invalidasi. Begitu Anda bisa menamai tiga bagian itu, setiap gejala terpetakan ke penyebab: unduhan kebesaran adalah masalah sizes, gambar basi adalah masalah TTL cache, dan disk penuh adalah masalah format-kali-varian. Komponen ini melakukan hal yang benar secara default cukup sering — tapi di proyek di mana gambar mendominasi payload, mengenal internal-nya adalah beda antara menebak dan memperbaiki.
Curl optimizer Anda sendiri dua kali — sekali dengan header Accept yang mengiklankan AVIF dan sekali tanpa — lalu bandingkan content-type dan content-length respons-nya. Menyaksikan negosiasi terjadi di server Anda sendiri mengajarkan lebih banyak daripada diagram mana pun.