Gambar & video di portal berita
Di portal berita, gambar biasanya 70–85% dari byte yang dikirim. Ini bagian di mana keputusan arsitektur langsung terlihat di tagihan.
Intisari
<Image>Astro mengoptimasi gambar yang di-importsaat build. Gambar dari database tidak termasuk.- Untuk gambar CMS:
image.domains/remotePatternsplus layanan pengubah ukuran. - Optimasi saat runtime di container-mu memakan CPU — pindahkan ke lapisan gambar.
- R2 menghapus biaya egress. Untuk portal dengan terabita gambar, ini penghematan terbesar tunggal.
widthdanheightwajib ada di tiap<img>— tanpanya CLS-mu rusak.
Tiga jenis gambar, tiga penanganan
| Jenis | Contoh | Penanganan |
|---|---|---|
| Aset build | Logo, ikon, ilustrasi tetap | src/assets/ + <Image> |
| Statis publik | favicon.ico, og-default.jpg | public/ |
| Dari database | Sampul artikel, foto berita | S3/R2 + pengubah ukuran |
Yang ketiga adalah 99% volume portalmu, dan yang paling sedikit dibahas tutorial.
Gambar build
---
import { Image } from "astro:assets";
import logo from "@/assets/logo.svg";
---
<Image src={logo} alt="Portal Contoh" width={160} height={40} loading="eager" />
Astro mengubah formatnya, membuat beberapa ukuran, memberi nama ber-hash, dan mengisi
width/height otomatis. Tidak ada yang perlu kamu pikirkan.
Gambar dari database
// astro.config.mjs
export default defineConfig({
image: {
domains: ["cdn.portal.contoh.id"],
remotePatterns: [{ protocol: "https", hostname: "**.portal.contoh.id" }],
},
});
---
import { Image } from "astro:assets";
---
<Image
src={artikel.gambarSampul}
alt={artikel.gambarAlt ?? artikel.judul}
width={1200}
height={675}
loading="eager"
fetchpriority="high"
/>
Ini akan bekerja — dan menghabiskan CPU container-mu. Untuk gambar remote, Astro mengunduh dan memprosesnya saat request memakai Sharp. Di 21 request per detik ke origin dengan beberapa gambar per halaman, itu beban CPU yang serius dan bersaing langsung dengan rendering halaman. Untuk portal berita, jangan optimasi gambar di container aplikasi.
Yang benar: lapisan gambar terpisah
Pembaca → Cloudflare → cdn.portal.contoh.id (R2 / S3)
└ pengubah ukuran: Cloudflare Images
atau imgproxy di container terpisah
---
// src/components/GambarArtikel.astro
interface Props {
path: string; // "2026/08/rupiah-menguat.jpg"
alt: string;
lebar?: number;
prioritas?: boolean;
}
const { path, alt, lebar = 1200, prioritas = false } = Astro.props;
const BASIS = "https://cdn.portal.contoh.id";
const ukuran = [400, 800, 1200, 1600].filter((u) => u <= lebar * 1.5);
const url = (w: number) => `${BASIS}/cdn-cgi/image/width=${w},format=auto,quality=82/${path}`;
---
<img
src={url(lebar)}
srcset={ukuran.map((w) => `${url(w)} ${w}w`).join(", ")}
sizes="(max-width: 700px) 100vw, 700px"
alt={alt}
width={lebar}
height={Math.round((lebar * 9) / 16)}
loading={prioritas ? "eager" : "lazy"}
fetchpriority={prioritas ? "high" : "auto"}
decoding="async"
/>
| Atribut | Kenapa wajib |
|---|---|
width + height | Browser memesan ruangnya sebelum gambar tiba — CLS nol |
srcset + sizes | Ponsel tidak mengunduh gambar 1600px |
format=auto | AVIF/WebP untuk yang mendukung, JPEG untuk sisanya |
loading="lazy" | Gambar di bawah lipatan tidak diunduh sampai perlu |
fetchpriority="high" | Untuk gambar sampul — ini biasanya elemen LCP-mu |
loading="lazy" pada gambar sampul artikel adalah kesalahan yang merusak LCP.
Gambar sampul hampir selalu elemen terbesar yang terlihat pertama — menundanya berarti menunda metrik
yang paling dinilai Google. Aturannya: gambar pertama yang terlihat eager dengan
fetchpriority="high"; semua sisanya lazy.
R2 vs S3: hitungannya
| S3 | R2 | |
|---|---|---|
| Penyimpanan | Per GB/bulan | Per GB/bulan, serupa |
| Egress | Per GB — mahal | Nol |
| Operasi | Per 1.000 | Per juta |
| Ke Cloudflare | Lewat internet publik | Di dalam jaringan Cloudflare |
Cache Cloudflare menyerap sebagian besar permintaan gambar, jadi egress origin tidak sebesar total bandwidth-mu. Tapi untuk arsip gambar yang jarang diakses — ratusan ribu foto berita lama — MISS terjadi terus-menerus, dan setiap MISS adalah egress berbayar dari S3. Di sanalah selisihnya menumpuk.
R2 bisa dipasang berdampingan dengan S3 tanpa migrasi besar. Aktifkan migrasi bertahap: R2 mengambil objek dari S3 saat pertama diminta dan menyimpannya. Setelah beberapa bulan, gambar yang benar-benar dipakai sudah pindah, dan yang tidak pernah diminta bisa diarsipkan ke Glacier.
Memigrasikan folder uploads/ CI3
# Inventarisasi dulu — angkanya biasanya mengejutkan
find /var/www/portal/uploads -type f | wc -l
du -sh /var/www/portal/uploads
# Berapa yang benar-benar dirujuk database?
mysql -N -e "SELECT DISTINCT gambar FROM artikel WHERE gambar IS NOT NULL" portal > /tmp/dipakai.txt
wc -l /tmp/dipakai.txt
# Salin dengan struktur folder dipertahankan
rclone copy /var/www/portal/uploads r2:portal-media/uploads \
--transfers 32 --checkers 64 --progress
Selisih antara jumlah berkas dan jumlah yang dirujuk biasanya besar. Folder uploads portal berumur penuh dengan berkas yatim: unggahan yang gagal, thumbnail dari plugin yang sudah tidak dipakai, duplikat dari editor yang mengunggah ulang. Memindahkan semuanya berarti membayar penyimpanan untuk berkas yang tidak pernah diminta. Tapi jangan hapus apa pun sebelum migrasi selesai — sebagian dirujuk dari dalam HTML badan artikel, bukan dari kolom database, dan kueri di atas tidak akan menemukannya.
# Cari juga rujukan di dalam badan artikel
mysql -N -e "SELECT isi FROM artikel" portal \
| grep -oE '/uploads/[^\"'\'' )]+' | sort -u > /tmp/dipakai-inline.txt
wc -l /tmp/dipakai-inline.txt
Video
| Pilihan | Kapan | Catatan |
|---|---|---|
| YouTube / Vimeo embed | Sebagian besar kasus | Gratis, tapi berat — pakai fasad |
| Cloudflare Stream | Video milik sendiri | Transcoding + pemutar termasuk |
| S3 + CloudFront | Sudah terlanjur di AWS | Perlu transcoding sendiri |
| Berkas MP4 langsung | Klip pendek saja | Tidak ada adaptasi bitrate |
---
// Fasad: gambar sampul dulu, iframe hanya setelah diklik
interface Props { id: string; judul: string; }
const { id, judul } = Astro.props;
---
<div class="video-fasad" data-yt={id} style="aspect-ratio:16/9">
<img src={`https://i.ytimg.com/vi/${id}/hqdefault.jpg`} alt={judul}
width="480" height="360" loading="lazy" />
<button aria-label={`Putar video: ${judul}`}>▶</button>
</div>
<script>
document.addEventListener("astro:page-load", () => {
document.querySelectorAll(".video-fasad").forEach((el) => {
el.querySelector("button")?.addEventListener("click", () => {
const id = (el as HTMLElement).dataset.yt;
el.innerHTML = `<iframe src="https://www.youtube-nocookie.com/embed/${id}?autoplay=1"
title="video" allow="autoplay; encrypted-media" allowfullscreen
style="width:100%;height:100%;border:0"></iframe>`;
}, { once: true });
});
});
</script>
Satu embed YouTube memuat sekitar 500 KB–1 MB JavaScript sebelum pembaca menekan play. Di
halaman artikel dengan tiga video, itu lebih berat daripada seluruh sisa halamanmu digabung. Fasad
menurunkannya jadi satu gambar sampul — dan pembaca yang tidak menonton tidak membayar apa pun. Perhatikan
juga youtube-nocookie.com: ia tidak menyetel cookie pelacakan sampai video diputar.
Latihan: hitung tiga angka untuk portalmu: jumlah berkas di uploads/, ukuran
totalnya, dan berapa banyak yang benar-benar dirujuk (gabungkan kedua kueri di atas). Lalu buat komponen
GambarArtikel.astro dengan srcset dan uji di Lighthouse — perhatikan skor LCP
sebelum dan sesudah menambahkan fetchpriority="high" pada gambar sampul.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.