UI flicker atau visual thrashing saat cold start merupakan cacat performa yang umum ditemui pada aplikasi React Native. Gejala paling umum adalah Flash of Unauthenticated Screen (FoUS), di mana layar Login muncul selama beberapa milidetik sebelum aplikasi mendeteksi token pengguna dan berpindah secara agresif ke Dashboard. Fenomena ini merusak user experience dan mengindikasikan masalah pada arsitektur state hydration.

Akar Masalah: Asynchrony State Hydration

Saat aplikasi React Native dijalankan dari kondisi cold start, runtime JavaScript diinisialisasi bersamaan dengan siklus hidup komponen React. Jika state aplikasi (seperti auth token atau preferensi tema) disimpan menggunakan storage engine asinkron konvensional seperti @react-native-async-storage/async-storage, alur eksekusi akan berjalan sebagai berikut:

  1. React engine mengeksekusi root component dan langsung merender pohon komponen berdasarkan initial state (misal: isAuthenticated: false).
  2. Engine storage mengirim permintaan I/O melalui serialisasi bridge atau JSI secara asinkron (mengembalikan Promise).
  3. Layar unauthenticated terlanjur di-paint ke layar natif perangkat.
  4. Promise storage resolve, memicu mutasi state global (isAuthenticated: true).
  5. React menjalankan reconciliation dan memicu re-render instan ke rute authenticated.

Selang waktu antara langkah 3 dan 5 menghasilkan flicker visual. UI meloncat secara kasar karena struktur hierarki layout dirombak mendadak.

Arsitektur Solusi

Untuk menghilangkan flicker sepenuhnya tanpa mengorbankan performa booting, terapkan tiga lapisan pengamanan:

  • Synchronous Storage: Ganti storage engine asinkron dengan react-native-mmkv. MMKV menggunakan memory-mapped file berbasis C++ library via JSI, memungkinkan pembacaan data disk secara synchronous langsung saat JavaScript thread menginisialisasi store.
  • Hydration Guard: State store harus mengekspos status hidrasi (_hasHydrated) secara eksplisit untuk mencegah navigator merender rute parsial.
  • Native Splash Gating: Gunakan react-native-bootsplash untuk menahan splash screen bawaan OS sampai hydration guard terkonfirmasi selesai, memastikan frame pertama yang dilihat pengguna adalah layout final.

Implementasi Store Zustand dan MMKV

Zustand menyediakan middleware persist dengan adapter kustom. MMKV menyediakan method getItem, setItem, dan removeItem yang sinkron.

// src/store/storage.ts
import { MMKV } from 'react-native-mmkv';
import { StateStorage } from 'zustand/middleware';

const mmkv = new MMKV({
  id: 'app-user-storage',
  encryptionKey: 'your-secure-key-if-needed',
});

export const clientStorage: StateStorage = {
  setItem: (name, value) => {
    mmkv.set(name, value);
  },
  getItem: (name) => {
    const value = mmkv.getString(name);
    return value ?? null;
  },
  removeItem: (name) => {
    mmkv.delete(name);
  },
};

Definisikan auth store dengan tracking hidrasi via callback onRehydrateStorage:

// src/store/useAuthStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { clientStorage } from './storage';

interface AuthState {
  token: string | null;
  isAuthenticated: boolean;
  _hasHydrated: boolean;
  setToken: (token: string | null) => void;
  setHasHydrated: (state: boolean) => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      token: null,
      isAuthenticated: false,
      _hasHydrated: false,
      setToken: (token) =>
        set({
          token,
          isAuthenticated: Boolean(token),
        }),
      setHasHydrated: (state) => set({ _hasHydrated: state }),
    }),
    {
      name: 'auth-storage',
      storage: createJSONStorage(() => clientStorage),
      onRehydrateStorage: () => (state) => {
        state?.setHasHydrated(true);
      },
    }
  )
);

Splash Screen Gating pada Root Navigator

Hubungkan _hasHydrated dengan rilis splash screen native. Layar root tidak boleh merender layout intermediate jika store belum terhidrasi.

// src/navigation/RootNavigator.tsx
import React, { useEffect } from 'react';
import { View } from 'react-native';
import BootSplash from 'react-native-bootsplash';
import { useAuthStore } from '../store/useAuthStore';
import { AppStack } from './AppStack';
import { AuthStack } from './AuthStack';

export const RootNavigator = () => {
  const hasHydrated = useAuthStore((state) => state._hasHydrated);
  const isAuthenticated = useAuthStore((state) => state.isAuthenticated);

  useEffect(() => {
    if (hasHydrated) {
      // Hilangkan native splash dengan animasi fade hanya saat hydration tuntas
      BootSplash.hide({ fade: true }).catch(() => {
        // Handle splash dismiss error
      });
    }
  }, [hasHydrated]);

  // Tahan render UI hierarki sebelum rehidrasi selesai
  if (!hasHydrated) {
    return <View style={{ flex: 1 }} />;
  }

  return isAuthenticated ? <AppStack /> : <AuthStack />;
};

Verifikasi Cold Start Profile

Untuk memastikan flicker tereliminasi dan waktu inisialisasi tetap optimal, lakukan verifikasi cold start langsung pada target device.

1. Android Cold Start Benchmark

Jalankan perintah ADB untuk mengukur waktu peluncuran aktivitas dalam status cold:

adb shell am force-stop com.yourapp.bundle
adb shell am start-activity -W -n com.yourapp.bundle/com.yourapp.bundle.MainActivity

Evaluasi metrik output:

  • ThisTime: Waktu inisialisasi Activity terakhir.
  • TotalTime: Waktu eksekusi dari intent peluncuran hingga frame pertama selesai di-draw. Pastikan angka ini tidak melonjak tajam setelah migrasi storage.

2. Visual Frame Inspection

Aktifkan Show layout bounds dan Profile HWUI rendering di menu Developer Options Android. Buka aplikasi dari app drawer. Layar splash harus bertransisi langsung ke stack yang sesuai (AppStack atau AuthStack) tanpa ada frame intervensi berwarna putih atau layout stack lain di baliknya.

Batasan dan Pertimbangan

Peringatan: Meskipun MMKV berjalan sinkron via JSI, jangan menyimpan payload berukuran sangat besar (misal: log lokal ratusan megabyte atau serialized nested cache) langsung ke dalam state rehidrasi UI. Pembacaan sinkron file besar pada UI thread saat engine inisialisasi dapat memicu dropped frames pada main thread rendering.

Gunakan storage sinkron khusus untuk state navigasi kritis: auth session, tokens, feature flags, dan user UI settings. Pindahkan data dokumen berskala besar ke database terisolasi seperti SQLite.