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
OrderRowyang dibuat kalau daftarnya pakaiScrollView+map, dan berapa kalau pakaiFlatListdengan 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 |
- Ukur di build rilis (
npx expo run:android --variant release,npx expo run:ios --configuration Release). Mode development menambah pemeriksaan yang membuat JavaScript jauh lebih lambat. - Tutorial lama menyuruh pakai Flipper dan Systrace. Integrasi Flipper sudah dinyatakan usang di React Native 0.73 dan dibuang dari template sejak 0.74, dan alat
systracesudah dicabut dari Android platform-tools. Penggantinya React Native DevTools dan profiler Android Studio (hasilnya bisa dibuka di Perfetto). console.logyang banyak juga memperlambat JS thread di build rilis.
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.
- Kalau JS thread sibuk (misalnya menjumlahkan 5.000 nota di setiap render), ketukan baru dilayani setelahnya: tombol terasa telat, dan baris baru di daftar yang digulir cepat muncul kosong dulu.
- Guliran dan animasi yang jalan di UI thread tetap mulus. Itu sebabnya badge #13 dianimasikan dengan Reanimated, yang worklet-nya jalan di UI thread (pelajaran animasi).
- Penyebab jank yang paling umum adalah render ulang berlebihan: komponen yang gak berubah ikut dihitung ulang tiap kali induknya render.
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}
/>
);
}
keyExtractor: kunci stabil per baris (idnota), jadi React bisa mencocokkan baris lama dan baru.getItemLayout: kalau tinggi baris sama, FlatList gak perlu mengukur tiap baris, dan bisa langsung lompat. Baris ke-4.000 ada di offset 72 × 3.999 = 287.928.- Tanpa
getItemLayout,scrollToIndex({ index: 3999 })ke baris yang belum pernah diukur gagal denganscrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, otherwise there is no way to know the location of offscreen indices or handle failures. windowSize={11}: jendela lebih kecil, memori lebih hemat, tapi guliran sangat cepat bisa memperlihatkan area kosong sesaat.
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 |
- Template Expo SDK 57 menyalakan React Compiler (
reactCompiler: true), yang memo komponen dan nilai secara otomatis, termasuk fungsirenderItemdi dalam komponen. Dokumentasi React menyarankan mengandalkan compiler untuk kode baru dan membiarkanmemo,useMemo, danuseCallbackyang sudah ada. Ukur dengan React Profiler sebelum dan sesudah, jangan menebak. useMemountuk hitungan berat yang dipakai saat render, misalnya total 5.000 nota, supaya gak dihitung ulang tiap ketukan.
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
- expo-image:
cachePolicybawaannya'disk'. Untuk foto yang muncul berulang di daftar,'memory-disk'. TambahkanrecyclingKey={order.id}di FlashList, danplaceholder(blurhash) supaya gak ada kotak kosong. - Ukuran bundle:
npx expo export --platform androiddi project ini (11 Oktober 2026) menghasilkan 1.243 modul dan bundle Hermes bytecode 2,7 MB (2.662.347 byte). Lihat isinya denganEXPO_ATLAS=true npx expo exportlalunpx expo-atlas .expo/atlas.jsonl, dan buang library yang gak perlu. - Layar lazy: async routes Expo Router belum didukung di build rilis native. Hermes juga sudah memetakan bytecode ke memori sebelum dijalankan, jadi memecah bundle kurang berpengaruh. Yang terasa: kerja yang dijalankan saat start. Jangan menaruh hitungan berat atau import library besar di
src/app/_layout.tsx.
⚠️ Jebakan yang sering ketemu
- Mengukur di mode development. Angkanya jauh lebih buruk dari build rilis. Ukur di build rilis, di HP Android kelas bawah, bukan cuma di iPhone baru.
ScrollView+mapuntuk ribuan baris. Semua baris dibuat sekaligus. PakaiFlatListatauFlashList.- Fungsi inline ke komponen yang di-
memo. Props selalu beda, jadimemogak pernah melewati render. Kirim fungsi yang stabil. scrollToIndextanpagetItemLayout. Gagal untuk baris yang belum diukur. IsigetItemLayoutkalau tinggi baris sama, atau tanganionScrollToIndexFailed.- Pakai Flipper atau Systrace dari tutorial lama. Pakai React Native DevTools dan profiler Android Studio atau Instruments.
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.