โ† Semua pembelajaran / Astro Nol โ†’ Portal Berita
Fase 11 ยท Performa & Capstone

Mengukur waktu render & kueri

Fase 10 menyuruhmu menghitung kapasitas dari waktu render. Ini cara mendapatkan angka itu, dan cara memperbaikinya kalau terlalu besar.

Sumber asli nodejs.org Resmi Rangkuman ~8 menit baca

Intisari

  • Pecah waktu render jadi bagian: kueri, panggilan API, dan render itu sendiri.
  • Ukur di produksi dengan sampling, bukan hanya di laptop.
  • Untuk halaman artikel, hampir selalu kueri yang mendominasi โ€” bukan render.
  • Server-Timing membuat rinciannya terlihat langsung di DevTools.
  • Optimasi tanpa pengukuran hampir selalu menyasar bagian yang salah.

Instrumentasi

// src/lib/ukur.ts
export class Pengukur {
  private bagian = new Map<string, number>();
  private mulai = performance.now();

  async ukur<T>(nama: string, fn: () => Promise<T>): Promise<T> {
    const t0 = performance.now();
    try {
      return await fn();
    } finally {
      const ms = performance.now() - t0;
      this.bagian.set(nama, (this.bagian.get(nama) ?? 0) + ms);
    }
  }

  header(): string {
    const bagian = [...this.bagian].map(([n, ms]) => `${n};dur=${ms.toFixed(1)}`);
    bagian.push(`total;dur=${(performance.now() - this.mulai).toFixed(1)}`);
    return bagian.join(", ");
  }

  ringkas() {
    return {
      total: Math.round(performance.now() - this.mulai),
      ...Object.fromEntries([...this.bagian].map(([n, ms]) => [n, Math.round(ms)])),
    };
  }
}
---
// src/pages/berita/[slug].astro
import { Pengukur } from "@/lib/ukur";

const u = new Pengukur();

const artikel = await u.ukur("db.artikel", () => ambilArtikelPublik(Astro.params.slug!));
if (!artikel) return Astro.rewrite("/404");

const [terkait, populer] = await Promise.all([
  u.ukur("db.terkait", () => ambilTerkait(artikel.kategoriId, 5)),
  u.ukur("db.populer", () => ambilPopuler(5)),
]);

// Terlihat di DevTools โ†’ Network โ†’ Timing
Astro.response.headers.set("Server-Timing", u.header());

// Sampling 1% ke log โ€” jangan catat semua di trafikmu
if (Math.random() < 0.01) {
  log("info", "render", { jalur: Astro.url.pathname, ...u.ringkas() });
}
---

Sampling 1% sudah cukup di 1,25 juta request per jam. Itu ~200 sampel per menit untuk halaman artikel โ€” lebih dari cukup untuk p95 yang stabil, dan tidak membanjiri CloudWatch. Mencatat semuanya akan menghasilkan tagihan log yang lebih besar daripada tagihan komputasimu.

Membaca hasilnya

Contoh rincian halaman artikel:

  db.artikel     18 ms   โ† kueri utama
  db.terkait     12 ms   โ† paralel dengan populer
  db.populer      9 ms
  render          6 ms   โ† Astro merender HTML
  total          37 ms

Kesimpulan: kueri = 84% dari waktunya. Mengoptimasi render tidak akan
mengubah apa pun; memperbaiki kueri terkait akan.
PolaArtinyaTindakan
Kueri > 70%Terikat databaseIndex, kurangi kueri, cache (Fase 2, 3)
Render > 50%Halaman terlalu kompleksKurangi elemen, potong daftar
API luar > 30%Bergantung pada pihak lainCache lebih agresif, atau pindah ke island
Total < 20 msSudah bagusKerjakan hal lain
p95 โ‰ซ p50Ada kasus yang jauh lebih lambatCari halaman mana; biasanya artikel sangat panjang

Menemukan kueri berlebih

// Hitung berapa kueri per request โ€” angka yang sering mengejutkan
let jumlahKueri = 0;

export function hitungKueri() { jumlahKueri++; }
export function resetKueri() { const n = jumlahKueri; jumlahKueri = 0; return n; }
// Di middleware
const res = await next();
const n = resetKueri();

if (n > 10) {
  log("warn", "kueri berlebih", { jalur: ctx.url.pathname, jumlah: n });
}

Halaman artikel yang butuh lebih dari lima kueri hampir selalu punya N+1 tersembunyi. Pola paling umum: mengambil daftar artikel terkait, lalu mengambil kategori untuk tiap satu di dalam loop. jsonArrayFrom dari Fase 2 menyelesaikannya dalam satu kueri.

Yang biasanya mahal, berurutan

BagianBiaya khasPerbaikan
Kueri tanpa index50โ€“2000 msEXPLAIN + index (Fase 2)
N+110 ms ร— jumlah barisjsonArrayFrom atau where in
COUNT(*) untuk paginasi20โ€“200 msHitungan perkiraan, atau tanpa total
Panggilan API luar berurutanJumlah latensinyaPromise.all (Fase 3)
Sanitasi HTML saat render5โ€“50 ms per artikelSanitasi saat menyimpan (Fase 9)
Optimasi gambar di container100โ€“500 msPindah ke lapisan gambar (Fase 8)
Merender daftar 500 item20โ€“100 msPotong; paginasi

Profil CPU saat ada yang tidak jelas

# Di container yang sedang jalan
aws ecs execute-command --cluster portal --task <id> --container web \
  --interactive --command "/bin/sh"

# Kirim SIGUSR1 untuk membuka port inspector
kill -USR1 1

# Dari laptop, lewat port forwarding SSM, buka chrome://inspect
# lalu rekam profil CPU selama 30 detik trafik nyata.

Lakukan ini di satu task saja, dan keluarkan dari target group lebih dulu kalau bisa. Membuka inspector di container produksi yang melayani trafik adalah hal yang bisa dilakukan sesekali untuk menelusuri masalah, tapi ia memperlambat proses dan membuka port yang seharusnya tidak terbuka. Jangan tinggalkan menyala.

Target yang masuk akal

Halamanp50p95
Artikel (MISS)< 40 ms< 120 ms
Beranda< 60 ms< 180 ms
Kategori< 50 ms< 150 ms
Server island paywall< 15 ms< 50 ms
Data perusahaan< 80 ms< 250 ms
Pencarian< 100 ms< 400 ms

Angka server island paling ketat karena ia berjalan untuk setiap pembaca halaman itu, bukan hanya saat cache miss (Fase 10). Sepuluh milidetik di sana lebih berharga daripada seratus milidetik di halaman yang di-cache.

Latihan: pasang Pengukur di halaman artikelmu dan buka DevTools โ†’ Network โ†’ Timing untuk melihat rincian Server-Timing. Catat p50 dan p95 tiap bagian setelah sehari sampling. Lalu pakai angka waktu render itu untuk menghitung ulang kapasitas ECS-mu dari Fase 10 โ€” kemungkinan besar berbeda dari perkiraan awalmu.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.