Dilema Realtime pada Lingkungan Mobile

Membangun fitur realtime di mobile berbeda secara fundamental dengan web browser. Pada React Native, runtime JavaScript berjalan di atas thread native yang terikat ketat pada batasan sistem operasi: penghematan daya (battery saver), network thrashing (perpindahan koneksi Wi-Fi ke seluler), dan penangguhan proses latar belakang (background suspension) oleh Android dan iOS.

Memilih antara WebSocket, Server-Sent Events (SSE), atau Push Notifications bukan sekadar preferensi sintaksis, melainkan kompromi struktural antara latensi, konsumsi baterai, ketahanan koneksi, dan biaya infrastruktur server.

Karakteristik Protokol dan Mekanisme Transport

1. WebSocket (RFC 6455)

WebSocket menginisiasi koneksi via HTTP Upgrade handshake lalu beralih ke protokol transport full-duplex dua arah melalui satu socket TCP persisten. Overhead frame sangat rendah (hanya 2-10 byte per payload setelah handshake), menjadikannya standar baku untuk use-case interaktif latensi rendah seperti aplikasi obrolan (chat), kolaborasi dokumen langsung, dan trading.

2. Server-Sent Events / SSE

SSE menggunakan transport HTTP standar (umumnya dengan Content-Type: text/event-stream) untuk streaming data searah dari server ke klien (unidirectional). Jika dijalankan di atas HTTP/2, SSE memanfaatkan multiplexing koneksi, mengurangi konsumsi resource TCP. SSE ideal untuk kasus pembacaan data seperti notifikasi real-time in-app, live tracking status kurir, atau update harga tanpa interaksi upstream melalui socket yang sama.

3. Background Push Notifications (FCM / APNs)

Berbeda dari dua protokol sebelumnya, push notification bukanlah koneksi langsung dari aplikasi Anda ke server aplikasi. Pengiriman dialihkan melalui arsitektur daemon OS terpusat (Apple Push Notification service atau Firebase Cloud Messaging). Aplikasi tidak perlu mempertahankan socket TCP aktif sendiri. Ini adalah satu-satunya mekanisme andal untuk sinkronisasi data saat aplikasi tertutup atau berada di background.

Siklus Hidup Aplikasi, Baterai, dan Network Thrashing

Faktor penentu utama pada mobile adalah bagaimana OS memperlakukan koneksi jaringan saat status aplikasi berubah:

  • iOS Suspension: Begitu aplikasi masuk ke background, OS hanya memberikan jeda beberapa detik sebelum menangguhkan (suspend) seluruh thread JavaScript dan jaringan. Socket WebSocket atau stream SSE akan diputus secara paksa oleh OS tanpa mengirim sinyal TCP FIN/RST yang bersih.
  • Android Doze Mode & App Standby: Android membatasi akses jaringan dan menahan CPU wake lock jika perangkat tidak bergerak. Sockets akan dibekukan, memicu half-open connection di sisi server.
  • Konsumsi Radio Jaringan (RRC): Menjaga socket TCP tetap hidup membutuhkan heartbeat berkala (ping-pong). Setiap pengiriman paket ping mengaktifkan modem radio seluler dari status Idle ke Connected/DCH, menguras baterai secara agresif jika interval heartbeat terlalu agresif (< 30 detik).

Aturan Desain: Jangan mempertahankan koneksi WebSocket atau SSE saat aplikasi berada di background. Putus koneksi secara eksplisit pada event background, lalu gunakan background push (silent notification/data-only message) untuk trigger fetch, dan sambungkan kembali socket saat aplikasi kembali ke foreground.

Beban dan Biaya Operasional Sisi Server

Perbedaan model koneksi berpengaruh drastis terhadap topologi dan kapabilitas horizontal scaling backend Anda:

Stateful (WebSocket)

Setiap klien memegang satu file descriptor TCP terbuka di memori instance backend. Hal ini memunculkan tantangan:

  • Load Balancer: Memerlukan layer-4 (TCP) proxy atau reverse proxy layer-7 dengan konfigurasi sticky sessions/ip_hash dan perpanjangan idle timeout.
  • Backplane Pub/Sub: Node server A tidak tahu jika penerima pesan terhubung ke Node server B. Anda wajib mengintegrasikan backplane seperti Redis Pub/Sub, Kafka, atau RabbitMQ untuk routing pesan antar-server instance.
  • Connection Storm: Saat deploy ulang server atau terjadi outage jaringan massal, jutaan koneksi klien akan melakukan reconnect secara serentak (thundering herd problem).

Stateless / Streaming (SSE & Push)

SSE tetap mempertahankan koneksi HTTP stream terbuka, namun integrasinya jauh lebih mudah dengan arsitektur load balancing HTTP standar. Sementara push notification mengalihkan beban koneksi sepenuhnya ke infrastruktur Apple dan Google; server Anda hanya mengeksekusi request HTTP POST stateless ke endpoint API FCM/APNs ketika ada event yang perlu dikirimkan.

Implementasi Resilien di React Native

Implementasi WebSocket pada React Native memerlukan auto-reconnect dengan exponential backoff, jitter, serta penanganan AppState untuk mencegah zombie socket.

import { useEffect, useRef, useCallback } from 'react';
import { AppState, AppStateStatus } from 'react-native';

const INITIAL_DELAY_MS = 1000;
const MAX_DELAY_MS = 30000;

export function useResilientWebSocket(url: string, onMessage: (data: string) => void) {
  const wsRef = useRef<WebSocket | null>(null);
  const reconnectAttempts = useRef(0);
  const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const isExplicitClosure = useRef(false);

  // ponytail: standard RN WebSocket used. Upgrade to native module transport if binary throughput is bottlenecked.
  const connect = useCallback(() => {
    if (wsRef.current?.readyState === WebSocket.OPEN) return;

    isExplicitClosure.current = false;
    const socket = new WebSocket(url);

    socket.onopen = () => {
      reconnectAttempts.current = 0;
    };

    socket.onmessage = (event) => {
      onMessage(event.data);
    };

    socket.onclose = () => {
      if (!isExplicitClosure.current) {
        scheduleReconnect();
      }
    };

    socket.onerror = () => {
      socket.close();
    };

    wsRef.current = socket;
  }, [url, onMessage]);

  const scheduleReconnect = useCallback(() => {
    if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);

    const baseDelay = Math.min(
      INITIAL_DELAY_MS * Math.pow(2, reconnectAttempts.current),
      MAX_DELAY_MS
    );
    // Tambahkan full jitter untuk mencegah thundering herd pada server
    const delayWithJitter = Math.floor(Math.random() * baseDelay);

    reconnectTimeoutRef.current = setTimeout(() => {
      reconnectAttempts.current += 1;
      connect();
    }, delayWithJitter);
  }, [connect]);

  const disconnect = useCallback(() => {
    isExplicitClosure.current = true;
    if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
    if (wsRef.current) {
      wsRef.current.close();
      wsRef.current = null;
    }
  }, []);

  useEffect(() => {
    connect();

    const subscription = AppState.addEventListener('change', (nextState: AppStateStatus) => {
      if (nextState === 'active') {
        connect();
      } else if (nextState === 'background' || nextState === 'inactive') {
        disconnect();
      }
    });

    return () => {
      subscription.remove();
      disconnect();
    };
  }, [connect, disconnect]);

  const sendMessage = useCallback((payload: string) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(payload);
    }
  }, []);

  return { sendMessage, disconnect };
}

Matriks Evaluasi Trade-off

ParameterWebSocketServer-Sent Events (SSE)Push Notifications (FCM/APNs)
Arah KomunikasiBidirectional (Full-duplex)Unidirectional (Server-to-Client)Unidirectional (Server-to-Client)
Latensi PengirimanSangat Rendah (<50ms)Rendah (<100ms)Variatif (ratusan ms hingga beberapa detik)
Dukungan Background OSTidak (Diputus saat suspend)Tidak (Diputus saat suspend)Ya (Ditangani daemon OS)
Dampak BateraiTinggi (jika ping interval rapat)SedangHampir Nol (terintegrasi OS)
Kompleksitas KlienTinggi (Heartbeat, Backoff, Sync)Sedang (Auto-reconnect bawaan)Rendah-Sedang (Native wrapper/SDK)
Beban InfrastrukturTinggi (Stateful, Memory, LB Layer 4)Sedang (Multiplexing HTTP/2)Rendah (Stateless server, cloud API)

Panduan Pengambilan Keputusan

Penerapan arsitektur realtime yang optimal biasanya mengombinasikan lebih dari satu protokol (hybrid architecture):

  1. Aplikasi Chat / FinTech / Live Gaming: Gunakan WebSocket saat foreground. Terapkan fallback ke Push Notification ketika pesan ditujukan kepada pengguna yang berada di status background/inactive.
  2. Live Tracking / Feed Monitoring / Metrics Dashboard: Gunakan SSE. Anda tidak memerlukan socket dua arah untuk data yang hanya dikonsumsi klien. Upstream aksi cukup menggunakan HTTP REST/RPC biasa.
  3. Update Asinkron / Konten Berkala: Gunakan Push Notification murni. Ketika data masuk, gunakan notifikasi background untuk memicu head-fetch cache lokal sehingga saat pengguna membuka aplikasi, data sudah tersedia.