← Semua pembelajaran / Astro Nol → Portal Berita
Fase 8 · Cloudflare: Cache, Edge & WAF

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.

Sumber asli docs.astro.build Resmi Rangkuman ~10 menit baca

Intisari

  • <Image> Astro mengoptimasi gambar yang di-import saat build. Gambar dari database tidak termasuk.
  • Untuk gambar CMS: image.domains / remotePatterns plus 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.
  • width dan height wajib ada di tiap <img> — tanpanya CLS-mu rusak.

Tiga jenis gambar, tiga penanganan

JenisContohPenanganan
Aset buildLogo, ikon, ilustrasi tetapsrc/assets/ + <Image>
Statis publikfavicon.ico, og-default.jpgpublic/
Dari databaseSampul artikel, foto beritaS3/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"
/>
AtributKenapa wajib
width + heightBrowser memesan ruangnya sebelum gambar tiba — CLS nol
srcset + sizesPonsel tidak mengunduh gambar 1600px
format=autoAVIF/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

S3R2
PenyimpananPer GB/bulanPer GB/bulan, serupa
EgressPer GB — mahalNol
OperasiPer 1.000Per juta
Ke CloudflareLewat internet publikDi 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

PilihanKapanCatatan
YouTube / Vimeo embedSebagian besar kasusGratis, tapi berat — pakai fasad
Cloudflare StreamVideo milik sendiriTranscoding + pemutar termasuk
S3 + CloudFrontSudah terlanjur di AWSPerlu transcoding sendiri
Berkas MP4 langsungKlip pendek sajaTidak 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.