Performance Optimization Mobile — Mobile

Performance Optimization di React Native Mobile device punya resource terbatas — CPU, RAM, battery. Optimasi performa bukan nice-to-have, tapi keharusan untuk…

Optimasi performa di React Native artinya menjaga aplikasi menjawab sentuhan dan menggulir tanpa tersendat. HP menggambar minimal 60 frame per detik, jadi tiap frame punya jatah paling lama 16,67 ms. Kalau kerjanya lewat jatah itu, satu frame terlewat dan layar terasa patah-patah (jank). Bayangin meja kasir Bu Rini: satu kasir (JavaScript thread) melayani semua permintaan, sementara ban berjalan pakaian (UI thread) terus bergerak sendiri. Kalau kasir sibuk menghitung 5.000 nota, pelanggan yang mau bayar menunggu, walau ban berjalannya tetap jalan.

🧪 Tebakan cepat: Tablet kasir Bu Rini membuka layar Riwayat berisi 5.000 nota setahun, tinggi tiap baris 72, area daftar setinggi 640. Berapa OrderRow yang dibuat kalau daftarnya pakai ScrollView + map, dan berapa kalau pakai FlatList dengan pengaturan bawaan? Jawabannya ada di bagian "Daftar panjang: FlatList".

Ukur dulu, baru ubah

Alat Untuk apa Cara membuka
React Native DevTools, tab React Profiler komponen mana yang render dan berapa lama tekan j di terminal npx expo start
React Native DevTools, panel Performance rekaman eksekusi JavaScript panel baru sejak React Native 0.83 (SDK 57 memakai 0.86)
Performance monitor FPS UI thread dan JS thread, langsung di HP menu developer, "Toggle performance monitor"
Android Studio Profiler, Xcode Instruments CPU, memori, dan trace sistem di sisi native dari Android Studio dan Xcode

Dua thread, satu frame

JavaScript thread menjalankan React, state, dan request. UI thread menggambar view native dan menggulir ScrollView. Performance monitor menampilkan dua angka FPS, satu per thread.

Daftar panjang: FlatList

ScrollView + map membuat semua baris sekaligus. FlatList cuma membuat baris di sekitar layar (virtualisasi). Nilai bawaannya dicek di kode React Native 0.86:

Prop Bawaan Artinya
initialNumToRender 10 baris yang dibuat di render pertama
maxToRenderPerBatch 10 baris tambahan per giliran
windowSize 21 jendela = 21 kali tinggi layar: layar yang terlihat plus sampai 10 layar di atas dan 10 di bawah
removeClippedSubviews true di Android, false di iOS lepas view yang keluar layar dari hierarki native

Untuk 5.000 nota, baris 72, area 640: ScrollView membuat 5.000 OrderRow sebelum layar pertama tampil. FlatList membuat 10 di render pertama, lalu menambah per 10 sampai jendelanya penuh. Di posisi paling atas, jendelanya 640 + 10 × 640 = 7.040, jadi sekitar 98 baris, dan angka itu gak naik walau datanya 50.000. Itu jawaban tebakannya.

// src/app/riwayat.tsx (mode kasir)
const TINGGI_BARIS = 72; // OrderRow: height 72

export function Riwayat({ nota }: { nota: Order[] }) {
  const buka = useCallback((id: string) => router.push(`/order/${id}`), []);

  return (
    <FlatList
      data={nota}
      keyExtractor={(o) => o.id}
      renderItem={({ item }) => <OrderRow order={item} onPress={buka} />}
      getItemLayout={(_, index) => ({ length: TINGGI_BARIS, offset: TINGGI_BARIS * index, index })}
      windowSize={11}
    />
  );
}

Render ulang: memo dan React Compiler

Saat #13 berubah jadi "Siap diambil", TanStack Query memberi array baru, tapi object order lain tetap object yang sama (structural sharing). Siapa yang render ulang tergantung dua hal. Pertama, tiap baris FlatList dibungkus sel yang PureComponent: kalau item dan renderItem-nya sama, sel itu dilewati. Kedua, memo(OrderRow) melewati render kalau semua props sama menurut Object.is. Dihitung untuk 98 baris yang terpasang, tanpa React Compiler:

renderItem OrderRow Yang render ulang
fungsi baru tiap render tanpa memo 98 baris
fungsi baru tiap render memo, tapi onPress={() => buka(item.id)} 98 baris: props-nya selalu beda
fungsi baru tiap render memo, onPress={buka} dari useCallback 1 baris: #13
stabil (useCallback) apa saja 1 baris: sel lain dilewati

FlashList

@shopify/flash-list memakai ulang view baris yang keluar layar alih-alih membuangnya. npx expo install di SDK 57 memasang 2.0.2 (di npm sudah 2.3.3). Versi 2 cuma jalan di New Architecture, sepenuhnya JavaScript (tanpa kode native), dan gak butuh estimatedItemSize lagi:

import { FlashList } from '@shopify/flash-list';

<FlashList data={nota} keyExtractor={(o) => o.id} renderItem={({ item }) => <OrderRow order={item} onPress={buka} />} />

Karena view dipakai ulang, gambar di baris perlu recyclingKey (lihat di bawah), supaya foto pelanggan sebelumnya gak tampil sekejap.

Gambar dan waktu buka aplikasi

⚠️ Jebakan yang sering ketemu

TL;DR: Satu frame punya jatah 16,67 ms; JS thread menjalankan React dan logika, UI thread menggulir dan menjalankan worklet Reanimated. Ukur dulu di build rilis: React Native DevTools (React Profiler, panel Performance sejak 0.83), "Toggle performance monitor", serta Android Studio Profiler dan Xcode Instruments; Flipper dan Systrace sudah ditinggalkan. Penyebab jank paling umum adalah render ulang berlebihan: renderItem yang stabil, atau memo dengan props stabil (useCallback), membuat cuma baris #13 yang render ulang, bukan 98, dan React Compiler di SDK 57 memo banyak hal secara otomatis. Untuk 5.000 nota, ScrollView membuat 5.000 baris, FlatList 10 lalu sekitar 98 (bawaan initialNumToRender 10, maxToRenderPerBatch 10, windowSize 21); getItemLayout membuat lompatan scrollToIndex jalan, keyExtractor memberi kunci stabil. FlashList 2 (2.0.2 di SDK 57) memakai ulang view, butuh New Architecture, tanpa estimatedItemSize. Pakai expo-image dengan cachePolicy dan recyclingKey, periksa bundle (2,7 MB, 1.243 modul di project ini) dengan Expo Atlas, dan jauhkan kerja berat dari layout akar.

Yang akan kamu pelajari