Belajar Axum - Performance & Benchmarking
Series/Belajar Axum/Episode 25
Episode 25 of 28

Belajar Axum - Performance & Benchmarking

Mengukur dan mengoptimalkan performa Axum: zero-copy, allocation tuning, biaya sebenarnya dari middleware stack, benchmark unit dengan criterion, dan load testing untuk keputusan berdasarkan data.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Aplikasi kalian bekerja — tetapi seberapa cepat? Episode ini membahas performance: mengukur dengan data (benchmark), menemukan bottleneck, dan mengoptimasi dengan teknik khas Rust/Axum: zero-copy dan allocation tuning. Prinsip utamanya: ukur dulu, baru optimasi — optimasi tanpa data adalah tebakan yang sering salah sasaran.

Mengapa performa perlu episode sendiri? Karena Axum sudah cepat secara bawaan (berdiri di atas hyper), sehingga bottleneck hampir selalu di pilihan kode kalian — string concatenation yang banyak, clone tak perlu, body yang di-buffer penuh, atau middleware yang menambah kerja. Mengenali pola-pola ini membuat aplikasi naik kelas tanpa arsitektur yang lebih rumit.

Pahami dulu: Di Mana Waktu Pergi

Sebelum mengoptimasi, petakan di mana request menghabiskan waktu:

100%

Dua hal sering jadi titik berat: database query (bisa ratusan ms) dan serialization JSON. Optimasi micro di handler kadang tidak terlihat dibanding query yang lambat. Selalu ukur.

Zero-Copy: Tidak Menyalin yang Tak Perlu

Zero-copy berarti data diproses tanpa disalin berulang. Contoh nyata di Axum:

Clone vs borrow
// JELEK: klon berlebihan per request
async fn bad_handler(name: String) -> String {
    let greeting = format!("Halo, {}!", name.clone()); // clone tak perlu
    let log_line = format!("handler dipanggil untuk {}", name); // clone lagi
    println!("{log_line}");
    greeting
}
 
// BAIK: pakai &str, tanpa clone
async fn good_handler(name: &str) -> String {
    format!("Halo, {name}!")
}

Hakikatnya: &str meminjam, String memiliki. Semakin sedikit alokasi heap baru per request, semakin murah handler-nya. format! adalah alokasi baru — gunakan bila perlu, bukan sebagai refleks.

Note

Zero-copy bukan dogma — ini trade-off. Kadang String (ownership) lebih masuk akal daripada &str yang rumit dengan lifetimes. Aturan praktis: hindari clone yang tak perlu dan alokasi ganda; jangan menjadikan "tidak pernah alokasi" sebagai tujuan mutlak.

Allocation Tuning yang Praktis

Alokasi heap adalah biaya; mengurangi jumlahnya membantu — terutama di hot path. Teknik umum:

Pre-allocate dengan capacity

Pre-allocate collection
// JELEK: tumbuh berulang (alokasi ganda)
let mut ids = Vec::new();
for id in 0..1000 {
    ids.push(id);
}
 
// BAIK: alokasi sekali dengan kapasitas
let mut ids = Vec::with_capacity(1000);
for id in 0..1000 {
    ids.push(id);
}

Vec::with_capacity memesan ruang sekali, menghindari re-allocation saat push melewati kapasitas. Untuk vector yang ukurannya bisa ditebak (jumlah baris hasil query, dll.), pre-allocate selalu menguntungkan.

Hindari format berulang di hot path

Format sekali, simpan
// BAIK: pola string disusun di startup, bukan per request
const WELCOME: &str = "Selamat datang";

Konstanta &'static str tidak mengalokasi di runtime — untuk pesan statis, ini lebih murah daripada format! yang dibangun ulang per request.

Biaya Middleware Stack

Middleware menambah kerja per request — bongkar-pasang header, parse, buffer. tower-http murah, tetapi tidak gratis:

Middleware dan biayanya
Router::new()
    .route("/", axum::routing::get(|| async { "ok" }))
    .layer(TraceLayer::new_for_http())        // ~mikrodetik, wajib
    .layer(CompressionLayer::new())           // mahal untuk body besar, hemat bandwidth
    .layer(TimeoutLayer::new(...))            // hampir gratis
    .layer(CorsLayer::new())                  // header extra

Aturan praktis:

  • TraceLayer dan TimeoutLayer — biaya kecil; pasang selalu.
  • CompressionLayer — biaya CPU untuk body besar. Untuk JSON API kecil, manfaat bisa kurang dari biayanya.
  • Custom middleware — tiap from_fn menambah lapisan; ukur dampaknya jika dipasang global.

Kuncinya: ukur aplikasi dengan dan tanpa lapisan untuk melihat biaya nyata, bukan menebak.

Benchmark Unit dengan Criterion

Criterion adalah standar benchmark Rust. Install sebagai dev-dependency:

Tambah criterion
cargo add --dev criterion

Tambahkan ke Cargo.toml:

Setup criterion
[[bench]]
name = "serialize"
harness = false

File benchmark:

benches/serialize.rs
use axum::Json;
use criterion::{criterion_group, criterion_main, Criterion};
use serde::Serialize;
 
#[derive(Serialize)]
struct Post {
    id: uuid::Uuid,
    title: String,
    body: String,
}
 
fn bench_json_serialize(c: &mut Criterion) {
    let post = Post {
        id: uuid::Uuid::new_v4(),
        title: "Judul".into(),
        body: "Isi".into(),
    };
 
    c.bench_function("serialize_post_json", |b| {
        b.iter(|| Json(&post))
    });
}
 
criterion_group!(benches, bench_json_serialize);
criterion_main!(benches);

Jalankan:

Jalankan benchmark
cargo bench

Criterion memberi interval kepercayaan dan — setelah run kedua — membandingkan perubahan performa antar commit. Gunakan ini untuk memverifikasi: "apakah optimasi ini benar-benar mempercepat?"

Warning

Jangan pernah benchmark di laptop dengan aplikasi lain berjalan atau dalam mode power-saver — hasilnya menyesatkan. Benchmark untuk perbandingan relatif (commit A vs B), bukan angka absolut. Angka absolut hanya bermakna di lingkungan produksi yang sama.

Load Test End-to-End

Criterion mengukur unit; load test mengukur aplikasi penuh dengan lalu lintas nyata. oha, bombardier, atau wrk adalah tool populer:

Load test dengan oha
# 1000 request, 50 konkuren
oha -n 1000 -c 50 http://localhost:3000/posts

Output yang perlu diperhatikan:

MetrikArti
Requests/secThroughput
Latency P50/P95/P99Distribusi waktu respons
Error rate% request gagal
TimeoutRequest melewati batas

Aturan load test:

  1. Test di environment yang sama dengan produksi — hasil di laptop tidak merepresentasikan server.
  2. Ukur P99, bukan rata-rata — rata-rata menyembunyikan tail latency yang dirasakan pengguna.
  3. Mulai dari baseline, naikkan bertahap — temukan titik jenuh, bukan sekadar "berapa request yang bisa ditangani".

Optimasi Nyata untuk Axum

Setelah data masuk, optimasi berdasarkan bukti:

1. Query Database (sering jadi pemenang)

Hanya ambil kolom yang dibutuhkan
// BAIK: tidak membawa body (bisa besar) untuk list
sqlx::query_as::<_, PostSummary>(
    "SELECT id, title FROM posts ORDER BY created_at DESC LIMIT 20",
)

Bawalah hanya kolom yang dipakai list view — SELECT * membawa data yang tidak dipakai dan memperbesar payload.

2. Pagination (bukan load semua)

Limit + offset dari episode 4/9 — aplikasi dengan 1 juta post tidak boleh mengembalikan semuanya.

3. Connection pool tuning

max_connections dari episode 9: terlalu kecil = antrian; terlalu besar = boros. Mulai max_connections = core * 2 + 1 (aturan umum untuk PostgreSQL) lalu tuning dengan load test.

4. Serialization: hindari intermediate

Serialisasi langsung
// BAIK: struct sudah Serialize, tidak perlu serde_json::json! manual
Ok(Json(post))

serde_json::json!({ "post": {...} }) membangun Value perantara lalu serialize lagi. Untuk hot path, serialize struct langsung.

Praktik: Siklus Optimasi

Urutan yang benar:

Siklus optimasi
# 1. Baseline
cargo bench
oha -n 5000 -c 50 http://localhost:3000/posts > baseline.txt
 
# 2. Identifikasi bottleneck (log latency dari episode 17)
# 3. Terapkan satu perubahan
# 4. Ulangi benchmark, bandingkan dengan baseline
# 5. Pertahankan jika terbukti lebih baik; revert jika tidak

Satu perubahan per iterasi — jika menerapkan tiga optimasi sekaligus, tidak ada yang tahu mana yang bekerja. Prinsip ini disebut one change at a time dan merupakan disiplin utama performance engineering.

Tip

Performance yang benar sering terlihat membosankan: query yang efisien, pagination, pool yang pas, dan menghindari alokasi tak perlu. Bukan "trik ajaib". Jika aplikasi kalian diukur dan temuan utamanya ada di database, optimasi handler 10 microsecond tidak akan menyelamatkan query 300ms.

Penutup

Pada episode 25 ini kalian bisa mengukur dan mengoptimasi:

  • Petakan waktu: query & serialization adalah titik berat paling umum.
  • Zero-copy & allocation tuning: &str vs String, with_capacity, hindari format berulang.
  • Biaya middleware: TraceLayer/Timeout murah, Compression & custom middleware diukur.
  • Criterion untuk benchmark unit + load test (oha) untuk end-to-end.
  • Optimasi nyata: kolom yang dibutuhkan, pagination, pool tuning, serialisasi langsung.
  • Disiplin: satu perubahan per iterasi, keputusan berdasarkan data.

Di episode 26 selanjutnya kita melihat cakrawala: ekosistem & tren modern 2026 — perkembangan Axum 0.8.9, axum-extra 0.12.x, perbandingan dengan Actix/poem/rocket, dan arah Rust di edge computing. Sampai jumpa di episode 26!