Di LOGIQUE, kami membangun aplikasi Flutter untuk klien yang butuh performa tinggi di lapangan. Artikel ini mendokumentasikan bagaimana tim kami menerapkan Flutter Rust FFI untuk image compression, dan berhasil mendapatkan speedup 26× dari baseline Dart. Repository: Source code benchmark lengkap tersedia di GitHub
Repository ini bisa langsung dijalankan secara lokal untuk membandingkan implementasi Dart dan Rust, lalu dites dengan kumpulan gambar milik sendiri. Pahami bagaimana kami mengimplementasikan Flutter Rust FFI untuk image compression yang jauh lebih cepat berikut ini.
Masalah Utama: Compression Jadi Bottleneck untuk UX
Image compression sebelum upload terdengar seperti langkah yang biasa saja. Ukuran file diperkecil, bandwidth lebih hemat, lalu upload jalan. Untuk kebanyakan aplikasi yang hanya menangani satu atau dua foto sekaligus, memang biasanya sesederhana itu. Tapi di kasus kami, user bisa mengunggah banyak foto dokumentasi dalam satu workflow, dan di titik itulah proses yang tadinya terlihat sepele berubah menjadi masalah UX yang serius.
Aplikasi yang kami bangun untuk klien ini dipakai di lapangan untuk mengunggah foto sebagai lampiran laporan. Pipeline-nya sederhana: pilih gambar → proses/compress → upload. Saat user hanya mengunggah dua atau tiga foto, tidak ada masalah yang terasa.
Masalah mulai muncul saat user mengunggah batch yang lebih besar dalam satu sesi. Karena semua gambar harus selesai diproses lebih dulu sebelum upload dimulai, user akhirnya hanya melihat loading screen cukup lama.
Insight penting: Dari sudut pandang user, aplikasi ini tidak terlihat sedang “melakukan image compression”, tapi seperti hang.
Yang Kami Coba Lebih Dulu di Dart
Sebelum pindah ke native code, kami mencoba dulu optimasi yang paling masuk akal di level Dart. Urutannya : ukur dulu, optimalkan stack yang ada, baru pertimbangkan solusi yang lebih jauh.
Menggunakan isolate untuk concurrency
Compression kami pindahkan ke isolate supaya UI thread tetap ringan. Hasilnya cukup terasa di sisi responsivitas. Animasi tetap mulus, tombol masih bisa ditekan, dan aplikasi tidak terasa macet. Tapi isolate tidak membuat algoritma compression-nya sendiri jadi lebih cepat. Isolate membantu concurrency dan responsiveness, dan dengan bounded parallelism throughput memang bisa naik, tapi tetap ada batasnya.
Resize lebih awal
Kami juga mencoba melakukan resize lebih awal di pipeline. Dengan begitu, data yang harus diproses di tiap tahap jadi lebih kecil. Ada peningkatan, tapi tidak besar.
Penyesuaian kualitas dan merapikan pipeline
Mengatur ulang compression setting dan menghapus langkah yang tidak perlu juga membantu mengurangi overhead. Ini tetap penting dilakukan, tapi hasilnya masih bersifat incremental.
Setelah semua optimasi itu, implementasi Dart kami turun dari 16,24 detik menjadi 8,16 detik untuk batch berisi 21 gambar pada mode release. Jelas lebih baik, tapi masih lebih lambat dari target yang kami inginkan.
Kenapa Dart Punya Batas di Kasus Ini
Setelah profiling, polanya mulai jelas. Image compression pada dasarnya adalah pekerjaan yang CPU-bound. Setiap gambar harus melewati beberapa tahap yang cukup berat:
- Decode raw image bytes menjadi representasi image di memory
- Resize gambar
- Encode ulang dengan quality dan constraint yang ditentukan
- Menghasilkan output tambahan seperti thumbnail atau varian hasil proses
Kalau dilihat satu gambar saja, waktunya mungkin tidak terlalu terasa. Tapi saat proses yang sama diulang untuk 21 gambar, total waktu tunggunya jadi sangat kelihatan. Jadi bottleneck-nya bukan lagi soal struktur kode atau cara kami menyusun pipeline, tapi memang throughput komputasi mentah yang tersedia di runtime Dart. Kalau ingin lebih cepat, kami harus turun lebih dekat ke level native.
Kenapa Memilih Rust, Bukan C atau Kotlin/Swift
Saat memutuskan memindahkan proses ini ke native code, opsinya cukup jelas: C/C++, Kotlin untuk Android, Swift untuk iOS, atau Rust. Kami memilih Rust karena empat alasan utama:
- Performanya setara dengan C/C++
Tidak ada managed runtime, langsung dikompilasi ke machine code, dan tidak ada pause karena GC - Memory safety dijaga saat compile time
Bug seperti buffer overflow atau use-after-free bisa dicegah sejak awal oleh compiler - Dukungan FFI bagus
Rust bisa mengekspor simbol yang kompatibel dengan C, dan itu bisa langsung dipanggil dari Flutter lewat dart:ffi - Ecosystem image processing-nya sudah matang
Crate image sudah cukup untuk menangani decode dan encode JPEG tanpa dependency tambahan
Selain itu, jauh lebih praktis merawat satu library Rust lintas platform dibanding harus menjaga implementasi Kotlin dan Swift secara terpisah agar tetap sinkron.
Arsitektur: Flutter Mengatur, Rust Mengeksekusi
Integrasi Flutter Rust FFI untuk image compression ini dirancang dengan surface area yang sesempit mungkin. Integrasinya dibuat sesederhana mungkin dengan pemisahan tanggung jawab yang jelas. Flutter tetap menangani semua hal yang berhubungan dengan produk dan aplikasi. Rust hanya mengerjakan transformasi yang berat secara komputasi.

Di project benchmark ini, kami memang sengaja mengirim raw image bytes melewati boundary FFI, bersama config pemrosesan yang kecil. Rust memproses bytes tersebut, lalu mengembalikan result buffer dan metadata ke Dart.
Implementasi
Bagian Dart : memanggil Rust via FFI
Flutter memuat library Rust yang sudah dikompilasi, lalu melakukan binding ke fungsi native untuk pemrosesan gambar. Yang dikirim adalah image bytes dan config kecil. Itulah surface area FFI yang sebenarnya di project ini.
typedef ProcessImageNative = Pointer<CProcessedImage> Function(
Pointer<Uint8> imageBytes,
Uint32 imageLen,
Uint32 maxWidth,
Uint32 maxHeight,
Uint8 quality,
Uint32 targetSizeKb,
Uint32 thumbnailSize,
);
final processImageFunc = _library
.lookup<NativeFunction<ProcessImageNative>>('rust_process_image')
.asFunction<Pointer<CProcessedImage> Function(
Pointer<Uint8>,
int,
int,
int,
int,
int,
int,
)>();
final result = processImageFunc(
imagePtr,
imageBytes.length,
config.maxWidth,
config.maxHeight,
config.quality,
config.targetSizeKb,
config.thumbnailSize,
);
Bagian Rust : mesin compression
Rust mengekspos fungsi yang kompatibel dengan C. Fungsi ini menerima raw image bytes beserta config, menjalankan pipeline native untuk pemrosesan gambar, lalu mengembalikan pointer ke struktur hasil proses.
#[no_mangle]
pub extern "C" fn rust_process_image(
image_bytes: *const c_uchar,
image_len: c_uint,
max_width: c_uint,
max_height: c_uint,
quality: c_uchar,
target_size_kb: c_uint,
thumbnail_size: c_uint,
) -> *mut CProcessedImage {
let run = || -> Result<*mut CProcessedImage, String> {
if image_bytes.is_null() {
return Err(String::from("image_bytes pointer is null"));
}
if image_len == 0 {
return Err(String::from("image_len is 0"));
}
let input = unsafe { std::slice::from_raw_parts(image_bytes, image_len as usize) };
let config = CompressionConfig {
max_width: clamp_u32(max_width, 1),
max_height: clamp_u32(max_height, 1),
quality: clamp_quality(quality),
target_size_kb: clamp_u32(target_size_kb, 1),
thumbnail_size: clamp_u32(thumbnail_size, 1),
};
let result = process_image(input, config)?;
Ok(convert_result_to_c(result))
};
match std::panic::catch_unwind(run) {
Ok(Ok(ptr)) => { clear_last_error(); ptr }
Ok(Err(message)) => { set_last_error(message); ptr::null_mut() }
Err(_) => { set_last_error("Rust panic while processing image"); ptr::null_mut() }
}
}
Hasil Benchmark
Kami membandingkan tiga implementasi pada batch yang sama, dijalankan dalam release build. Berikut perbandingan performa Flutter Rust FFI image compression vs implementasi Dart murni
Ukuran batch: 21 gambar
| Implementasi | Total Waktu | Rata-rata/gambar | Gambar/detik | Speedup |
|---|---|---|---|---|
| Dart Original | 16.24s | 773.1ms | 1.29 | 1.00x |
| Dart Optimized | 8.16s | 388.4ms | 2.57 | 1.99x vs baseline |
| Rust via FFI | 624 ms | 29.7ms | 33.65 | 26.02x vs baseline |
Project benchmark lengkap yang dipakai untuk pengukuran ini tersedia di repository GitHub di atas, jadi siapa pun bisa mencoba menjalankan benchmark yang sama di device sendiri.
Screenshot benchmark dari release build
Screenshot di bawah ini adalah hasil benchmark dari release run yang sama dengan angka pada tabel di atas. Fungsinya sebagai bukti visual bahwa angka yang ditampilkan memang berasal dari hasil run nyata di device, bukan estimasi.
Dart (Baseline)

Dart (Optimized)

Rust (FFI)

Speedup Summary

Apa Arti Angka-Angka Ini Sebenarnya
Ada beberapa hal penting yang perlu dicatat.
Pertama, optimasi di sisi Dart bukan usaha yang sia-sia. Turun dari 16,24s ke 8,16s sudah merupakan peningkatan yang berarti. Ini juga menunjukkan bahwa concurrency berbasis isolate memang bisa membantu throughput jika dipakai dengan cara yang tepat.
Kedua, lonjakan terbesar memang datang dari Rust. Pada release run ini, jalur Rust via FFI menyelesaikan batch 21 gambar yang sama dalam 624ms, yaitu 26,02× lebih cepat dibanding baseline Dart dan 13,07× lebih cepat dibanding versi Dart yang sudah dioptimasi.
Ketiga, benchmark ini terutama menunjukkan peningkatan throughput, bukan otomatis menunjukkan rasio compression yang lebih baik untuk dataset ini.
Catatan penting: pada run ini, total output hasil proses justru lebih besar daripada total input aslinya. Hal seperti ini bisa terjadi jika gambar sumbernya memang sudah kecil atau sudah cukup efisien hasil kompresinya, lalu di-encode ulang dengan setting yang dipakai sekarang. Jadi hasil utama yang perlu dilihat di sini adalah peningkatan kecepatan, bukan penghematan ukuran file.
Kapan Tetap Pakai Dart, Kapan Perlu Rust
Kasus ini bukan berarti semua hal perlu dipindahkan ke Rust. Pendekatan yang tepat justru lebih selektif. Tetap gunakan Dart untuk hal-hal yang memang cocok dikerjakan di sana, lalu pindahkan ke Rust hanya kalau profiling benar-benar menunjukkan ada bottleneck CPU yang nyata.
Tetap di Dart kalau:
- User biasanya hanya upload 1–3 gambar
- Performa saat ini sudah cukup
- Kesederhanaan implementasi lebih penting daripada performa mentah
- Compression bukan operasi yang sering terjadi
Mulai pertimbangkan Rust kalau:
- Ukuran batch besar,
- Profiling menunjukkan bottleneck CPU-bound
- Waktu tunggu mulai merusak pengalaman user
- Operasinya murni komputasi dan tidak berhubungan langsung dengan UI
Hal Penting yang Kami Pelajari
- Ukur dulu sebelum optimasi.
Dari hasil profiling, bottleneck-nya terlihat jelas dan bisa diukur. Tanpa itu, mudah sekali salah menebak sumber masalahnya. - Maksimalkan stack yang ada lebih dulu.
Optimasi di Dart tetap perlu dilakukan, dan hasilnya hampir memangkas runtime jadi setengah. - Isolate sangat membantu, tapi bukan solusi akhir.
Isolate bisa menjaga responsiveness dan meningkatkan throughput, tapi untuk workload CPU-heavy yang berulang, native execution tetap unggul. - Jaga surface area FFI tetap kecil dan eksplisit.
Boundary yang sempit dan jelas membuat integrasi jauh lebih mudah dipahami dan dipelihara. - Rust + Flutter layak dipakai di production.
Dengan error handling yang rapi dan aturan ownership yang jelas, kombinasi ini sangat kuat untuk optimasi hot path.
Penutup
Awalnya image processing hanya terlihat seperti bagian kecil dari fitur upload. Tapi begitu ukuran batch membesar, bagian kecil itu berubah menjadi bottleneck performa yang serius.
Solusinya bukan rewrite aplikasi, dan bukan juga pindah framework. Yang kami lakukan adalah mencari bagian yang benar-benar CPU-constrained, memindahkan hanya bagian itu ke Rust, dan membiarkan bagian lain tetap seperti semula.
Dengan pendekatan Flutter Rust FFI, image compression yang semula memakan 16,24 detik bisa diselesaikan dalam 624ms. Di project benchmark ini, latensi batch turun dari 16,24 detik pada baseline Dart menjadi 624ms dengan Rust via FFI di mode release. Itu berarti ada peningkatan throughput sebesar 26,02× dalam run yang sama.
Kadang keputusan arsitektur terbaik bukan soal mengganti semuanya, tapi soal tahu bagian mana dari stack yang memang butuh alat yang lebih tepat, lalu cukup mengubah bagian itu saja.
Jika tim Anda sedang menghadapi bottleneck performa serupa di Flutter, atau butuh bantuan memutuskan kapan perlu turun ke native code, tim engineer LOGIQUE terbuka untuk diskusi. Hubungi kami untuk mulai berdiskusi.
