Pada aplikasi React Native skala menengah hingga besar, skenario umum seperti perpindahan ke tab dashboard sering memicu render bersamaan pada beberapa komponen independen (misalnya avatar header, widget saldo, dan banner notifikasi). Jika masing-masing komponen memanggil endpoint data profil pengguna (/api/v1/profile) saat mount, aplikasi akan mengirimkan sejumlah HTTP request identik ke server dalam rentang beberapa milidetik. Fenomena ini memicu thundering herd problem atau cache stampede di sisi client, yang mengakibatkan lonjakan penggunaan bandwidth seluler, latensi render, dan potensi respons HTTP 429 Too Many Requests dari API gateway.
Akar Masalah: Thundering Herd di React Native
Cache stampede di lingkungan mobile umumnya berakar dari tiga kondisi:
- Siklus Hidup Komponen Terisolasi: Komponen UI dirancang modular dengan pemanggilan data mandiri via
useEffectatau custom hook tanpa sinkronisasi global. - Cold Start & Cache Miss Serentak: Saat cache lokal kedaluwarsa atau belum terbentuk, seluruh komponen mengevaluasi bahwa data tidak ada dan secara bersamaan memicu network request.
- Asinkronitas Tanpa Kunci (Locking Mechanism): Request pertama belum selesai dieksekusi (masih berstatus pending), sehingga request kedua dan seterusnya tidak mendeteksi bahwa proses pengambilan data sedang berlangsung.
Arsitektur Solusi: In-Flight Promise Deduplication
Solusi paling efisien untuk masalah ini adalah in-flight request deduplication. Mekanisme ini menahan referensi Promise yang sedang berjalan di dalam memori transient (Map). Setiap kali komponen meminta data dengan kunci identik:
- Jika
Promiseuntuk kunci tersebut sedang berjalan, kembalikan referensiPromiseyang sama. - Jika tidak ada
Promiseaktif, buat request baru, daftarkan keMap, dan hapus dariMapsegera setelah selesai (baik sukses maupun gagal).
Penyimpanan persisten lokal ditangani oleh MMKV (via react-native-mmkv) karena operasi sinkron dan latensi I/O yang sangat rendah dibanding AsyncStorage.
Implementasi: Dedupe Client Terintegrasi MMKV
Berikut implementasi client fetcher dengan proteksi cache stampede dan SWR (Stale-While-Revalidate):
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
interface CacheEnvelope<T> {
data: T;
cachedAt: number;
ttl: number;
}
interface FetchOptions {
ttl?: number; // Waktu validitas data segar (ms)
staleTtl?: number; // Batas toleransi data basi saat revalidasi (ms)
}
// In-memory registry untuk request yang sedang berjalan
const inFlightRequests = new Map<string, Promise<any>>();
export async function dedupeFetch<T>(
key: string,
fetcher: () => Promise<T>,
options: FetchOptions = {}
): Promise<T> {
const { ttl = 60000, staleTtl = 300000 } = options;
const now = Date.now();
// 1. Periksa Cache Persisten MMKV
const cachedRaw = storage.getString(key);
if (cachedRaw) {
try {
const envelope: CacheEnvelope<T> = JSON.parse(cachedRaw);
const age = now - envelope.cachedAt;
// Data masih sepenuhnya segar
if (age <= ttl) {
return envelope.data;
}
// Data basi tetapi masih dalam jendela stale-while-revalidate
if (age <= ttl + staleTtl) {
// Trigger background revalidation jika belum ada request berjalan
revalidateInBackground(key, fetcher, ttl);
return envelope.data;
}
} catch {
storage.delete(key);
}
}
// 2. Evaluasi In-Flight Promise Pool
const existingPromise = inFlightRequests.get(key);
if (existingPromise) {
return existingPromise as Promise<T>;
}
// 3. Eksekusi Request Baru dengan Lifecycle Safe Cleanup
const requestPromise = (async () => {
try {
const result = await fetcher();
const envelope: CacheEnvelope<T> = {
data: result,
cachedAt: Date.now(),
ttl,
};
storage.set(key, JSON.stringify(envelope));
return result;
} finally {
// Wajib: Bersihkan pool agar tidak terjadi memory leak atau deadlock
inFlightRequests.delete(key);
}
})();
inFlightRequests.set(key, requestPromise);
return requestPromise;
}
function revalidateInBackground<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number
): void {
if (inFlightRequests.has(key)) return;
const bgPromise = fetcher()
.then((result) => {
const envelope: CacheEnvelope<T> = {
data: result,
cachedAt: Date.now(),
ttl,
};
storage.set(key, JSON.stringify(envelope));
})
.catch((err) => {
// Log error background tanpa menginterupsi UI
console.warn(`Revalidation failed for ${key}:`, err);
})
.finally(() => {
inFlightRequests.delete(key);
});
inFlightRequests.set(key, bgPromise);
}
Penanganan Lifecycle dan Pencegahan Deadlock
Kesalahan fatal dalam implementasi promise pool adalah membiarkan promise berstatus rejected tersimpan di dalam Map. Jika blok finally diabaikan:
- Komponen yang memanggil fungsi berikutnya akan terus menerima rejected promise yang sama tanpa pernah mencoba network call baru.
- Aplikasi mengalami kondisi pseudo-deadlock, di mana data tidak pernah diperbarui hingga aplikasi di-restart secara penuh (proses JS bundle dimulai ulang).
Penggunaan blok finally menjamin pembersihan kunci dari inFlightRequests secara deterministik, baik saat HTTP call sukses, timeout, maupun mengalami network failure (e.g., DNS resolution drop).
Panduan Konfigurasi Stale-While-Revalidate TTL
Arsitektur mobile berhadapan langsung dengan latensi koneksi radio seluler yang fluktuatif. Pola konfigurasi TTL yang direkomendasikan dibagi berdasarkan karakteristik data:
- Data Statis/Katalog (Category, Config, Assets):
ttl: 3600000(1 jam),staleTtl: 86400000(24 jam). Memastikan UI langsung muncul saat cold start. - Data Pengguna Semi-Dinamis (Profile, Entitlements):
ttl: 60000(1 menit),staleTtl: 300000(5 menit). Mengeliminasi request saat navigasi antar tab layar. - Data Finansial/Transaksi Kritis (Balance, Cart):
ttl: 0,staleTtl: 0. Melewati storage MMKV secara langsung, namun tetap memanfaatkan in-flight dedupe untuk meredam pemanggilan serentak dari multiple mounts.
Verifikasi Hasil Profiling Network
Efektivitas mekanisme ini dapat diverifikasi menggunakan Reactotron, Flipper Network Plugin, atau Charles Proxy:
Baseline (Tanpa Dedupe): 4 komponen me-mount halaman dashboard secara bersamaan. Inspector mencatat 4 koneksi HTTP terpisah ke endpoint
/api/v1/profiledengan status 200/429, durasi rata-rata 320ms per request, dan bandwidth terbuang 4x lipat.
Setelah Dedupe Aktif: Dari 4 komponen yang me-mount bersamaan, hanya tercatat 1 HTTP request fisik pada inspector. Tiga pemanggil lainnya menerima resolusi dari memory promise yang sama secara lokal dengan waktu eksekusi < 1ms setelah data tiba.
Pendekatan ini memangkas round-trip time jaringan secara drastis pada cold start dan sepenuhnya menghilangkan risiko rate-limiting 429 akibat multi-mount komponen di React Native.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!