Mengukur waktu render & kueri
Fase 10 menyuruhmu menghitung kapasitas dari waktu render. Ini cara mendapatkan angka itu, dan cara memperbaikinya kalau terlalu besar.
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-Timingmembuat 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.
| Pola | Artinya | Tindakan |
|---|---|---|
| Kueri > 70% | Terikat database | Index, kurangi kueri, cache (Fase 2, 3) |
| Render > 50% | Halaman terlalu kompleks | Kurangi elemen, potong daftar |
| API luar > 30% | Bergantung pada pihak lain | Cache lebih agresif, atau pindah ke island |
| Total < 20 ms | Sudah bagus | Kerjakan hal lain |
| p95 โซ p50 | Ada kasus yang jauh lebih lambat | Cari 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
| Bagian | Biaya khas | Perbaikan |
|---|---|---|
| Kueri tanpa index | 50โ2000 ms | EXPLAIN + index (Fase 2) |
| N+1 | 10 ms ร jumlah baris | jsonArrayFrom atau where in |
COUNT(*) untuk paginasi | 20โ200 ms | Hitungan perkiraan, atau tanpa total |
| Panggilan API luar berurutan | Jumlah latensinya | Promise.all (Fase 3) |
| Sanitasi HTML saat render | 5โ50 ms per artikel | Sanitasi saat menyimpan (Fase 9) |
| Optimasi gambar di container | 100โ500 ms | Pindah ke lapisan gambar (Fase 8) |
| Merender daftar 500 item | 20โ100 ms | Potong; 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
| Halaman | p50 | p95 |
|---|---|---|
| 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.