Cara mengukur dan meningkatkan performance aplikasi berbasis React Native

M Nurilman Baehaqi
9 min readJun 27, 2022

--

Ilustrasi dari Marc-Olivier Jodoin

React Native merupakan framework open source yang dibuat Facebook (Meta) untuk membuat aplikasi berbasis React secara cross platform. Hal ini berarti kita bisa menulis kode sekali dan bisa dijalankan di beberapa platform seperti Android, iOS, dan tvOS. Saat ini sudah banyak aplikasi yang dibuat menggunakan framework React Native, di antaranya Facebook, Coinbase, Shopify, Tesla, Traveloka dan Flip. React Native sampai tulisan ini dibuat masih cukup populer dengan 103 ribu stars di repository Githubnya.

Salah satu aspek yang penting dalam pengembangan aplikasi adalah masalah performance. Sebelum berbicara lebih jauh tentang pefrormance, kita harus tahu dulu apa sih yang dimaksud dengan performance yang ideal dalam sebuah aplikasi?

Aplikasi yang performancenya bagus adalah ketika proses, event, dan feedback terjadi dalam waktu yang relatif cepat. Dalam aplikasi React Native sendiri, diharapkan kita bisa mencapai 60 fps (frame per second). Ketika terjadi heavy process dalam aplikasi kita (misalnya ada rendering sampai 200ms), ada sekitar 12 frame yang drop dan ada animasi yang sedang berjalan pada saat itu, maka dia akan terasa tidak responsive. Idealnya, exection time suatu proses tidak lebih dari 100ms. Jika lebih dari itu, maka user akan merasa kalau aplikasi yang digunakan itu lambat. Jadi kalau dari perspektif user, mereka menginginkan response yang cepat ketika melakukan event seperti mengklik tombol Login atau melihat daftar transaksi yang mereka miliki tanpa ada semacam delay.

Selain frame rate, kita harus juga mengetahui JS Thread. Ini adalah thread di mana most of API, React business logic dan animasi diproses. Gambar berikut menunjukkan arsitektur yang dimiliki oleh React Native (old architecture).

Image from BAM Tech (https://blog.bam.tech/developer-news/measuring-and-improving-performance-on-a-react-native-app#:~:text=To%20measure%20performance%2C%20it's%20advised,released%20version%20of%20your%20app.)

Lalu pertanyaannya adalah ketika kita sudah merasa aplikasi ini lambat, apa yang harus kita lakukan?

Menentukan pain point

Ini merupakan bagian awal dari proses debugging performance, yaitu mengidentifikasi masalah yang dirasakan baik oleh developer maupun feedback user. Kita harus menentukan di bagian mana saja sih masalah ini terjadi? Apakah ada spesifik use case di mana aplikasi terasa lebih lambat?

Output dari bagian ini adalah suatu dokumen yang berisi use case dan poinnya. Poin ini dapat ditentukan dengan melihat seberapa banyak yang berpotensi merasakannya dan seberapa besar kerugian yang berpotensi dialami. Semakin besar, maka bobot poinnya akan semakin besar pula. Dengan melakukan ini, kita bisa lebih terarah dalam menangani issue performa yang muncul.

Mengukur seberapa lambat aplikasi kita

In-display Profiler

Jika aplikasi kita berjalan di debug mode, kita bisa mengaktifkan fitur Perf Monitor dengan membuka menu RN debugging tools. Jika sudah aktif, maka akan muncul overlay seperti gambar berikut

Bagian RAM menunjukkan penggunaan memori untuk proses yang saat ini sedang berjalan, sedangkan bagian JSC menampilkan penggunaan memori JavaScript Thread (tempat most of rendering process, API Call dan Animasi). Semakin besar valuenya, maka semakin lambat aplikasinya.

Bagian Views memiliki dua angka: angka pertama menunjukkan jumlah view yang saat ini terlihat, dan angka kedua menampilkan jumlah view yang dibuat dan disimpan dalam memori. Tujuan dari dua bagian terakhir adalah untuk menunjukkan kecepatan frame saat ini dan frame per second untuk JavaScript Thread. Jadi, kita bisa tahu bagian mana yang membuat frame menjadi drop.

React Profiler API

Sejak release React Native 0.61.0, React Profiler API sudah stable dan dapat digunakan oleh para developer. Profiler ini bentuknya HOC (High Order Component) yang akan menghasilkan informasi dalam bentuk callback prop:

function onRenderCallback(id, // the “id” prop of the Profiler tree that has just committedphase, // either “mount” (if the tree just mounted) or “update” (if it re-rendered)actualDuration, // time spent rendering the committed updatebaseDuration, // estimated time to render the entire subtree without memoizationstartTime, // when React began rendering this updatecommitTime, // when React committed this updateinteractions // the Set of interactions belonging to this update)

Tantangannya adalah kita harus membungkus komponen yang akan kita cari tau informasi performance-nya satu persatu. Ini cukup merepotkan karena goal utama kita adalah menginspect secara keseluruhan.

React DevTools

Karena Profiler API kurang efisien dalam mendapatkan informasi performance, akhirnya setelah melakukan pencarian tentang performance tools, dapatlah sebuah open source project yang bernama React DevTools. Tools ini dibuat spesial supaya bisa mendebug aplikasi React yang tidak berjalan di browser seperti React Native atau webview.

Cara installnya tinggal jalani command berikut di root project aplikasi kita

yarn global add react-devtools atau npm install -g react-devtools

Kemudian, tinggal kita koneksikan device dengan port yang digunakan di React DevTools

adb reverse tcp:8097 tcp:8097

Setelah itu tinggal jalanin command react-devtools dan tunggu sampai muncul window baru tampilan user interfacenya. Setelah kita klik Recording untuk beberapa saat dan kita bisa mulai melakukan action di device seperti klik button, hasilnya akan seperti ini.

Kita bisa memilih komponen yang akan diinspect: seberapa kali dia re-render, berapa ms setiap kali render, dan apa yang menyebabkan re-render. Informasi ini lah yang sangat dibutuhkan dan kita bisa tahu seberapa lambat aplikasi kita. Usahakan bahwa rendering time tidak lebih dari 100ms. Kita juga bisa tahu komponen apa yang paling berat (ditandai dengan warna kuning keemasan).

Leak Canary

LeakCanary merupakan library untuk mendeteksi memory leak untuk Android. Jadi kita bisa tahu bagian mana aja yang menyebabkan issue Application Not Responding atau crash OutOfMemoryError.

Image from https://square.github.io/leakcanary/

Untuk menggunakan LeakCanary, cukup kita tambahkan dependencyleakcanary-android di filebuild.gradle (level app):

dependencies {// debugImplementation because LeakCanary should only run in debug builds.debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.9.1’}

Setelah itu build aplikasi kita di mode debug. LeakCanary ini cara kerjanya adalah dengan mendeteksi secara otomatis lalu memberi tahu memory leaks di 4 tempat: retained object seperti Activity atau Fragment, dumping Java heap, analisis heap itu, dan mengkategorisasikan leaks itu sendiri. Lebih lengkapnya bisa baca dokumentasinya tentang “How LeakCanary works”.

Canary leaks bisa diintegrasikan dengan Flipper (debugging tools punya Meta) yang dapat diinstal sebagai plugin tambahan.

Instruments

Ini merupakan tools debugging yang dibuat spesial oleh Apple via XCode. Instruments memiliki beberapa fitur diantaranya Time Profiling (bisa melihat eksekusi native code dalam bentuk Call Tree), Energy log, App Launch, dan lain-lain. Kita bisa menggunakan ini untuk profiling iOS apps.

Image from https://www.raywenderlich.com/16126261-instruments-tutorial-with-swift-getting-started

Teknik-teknik untuk meningkatkan performance

Bagian ini merupakan lanjutan dari 2 step di atas. Tentunya, setelah tahu “oh, aplikasi kita lambat nih”, kita pasti harus memperbaikinya. That’s just how software engineer do: solve the problem.

Memoization

Dalam dunia pemrograman, memoization adalah teknik membuat aplikasi lebih efisien, optimal dan cepat. Memoization dilakukan dengan menyimpan hasil komputasi dalam cache, dan mengambil informasi yang sama dari cache pada saat diperlukan, bukan menghitungnya lagi.

Dengan kata yang lebih sederhana, memoization terdiri dari penyimpanan dalam cache output suatu fungsi, dan membuat fungsi tersebut memeriksa apakah setiap perhitungan yang diperlukan ada dalam cache sebelum menghitungnya kembali.

Cache hanyalah penyimpanan data sementara yang menyimpan data sehingga kode berikutnya untuk data tersebut dapat dieksekusi lebih cepat.

Memoisasi adalah trik sederhana namun lumayan powerful yang dapat membantu mempercepat kode kita, terutama saat menangani fungsi komputasi yang berulang dan berat.

React memiliki built-in memo function seperti useMemo, useCallback dan memo.

useCallback merupakan memo yang dapat menghindari re-define function di setiap render. Kalo kita punya function yang besar misalnya, kita bisa memanfaatkan useCallback sehingga Javascript akan mereturn function yang sama di setiap render.

useMemo merupakan function seperti useCallback namun ia bekerja untuk menyimpan dan atau mengembalikan suatu value (bukan function).

memo merupakan HOC yang bisa memoize output dari suatu komponen. Ini sangat powerful untuk mencegah re-rendering ketika input yang dipassing (props) itu sama. Dengan melakukan ini, kita bisa meningkatkan performance 40%–70% tergantung komponennya. Namun, tidak semua komponen harus dimemo karna bukannya meningkatkan peformance, justru bisa berdampak sebaliknya. Jadi kita harus bijak dalam me-wrap komponen dalam suatu memo.

Image form https://dmitripavlutin.com/use-react-memo-wisely/

Contoh penggunaan:

Using Fast Image

Fast Image merupakan library untuk optimasi gambar dengan mengoptimallkan caching menchanism. Berdasarkan pengalaman, menggunakan fast image daripada image React Native dapat meningkatkan performa sekitar 30–60% tergantung pada gambar dan jumlah gambar pada suatu komponen. Kasus yang sangat berguna adalah ketika kita punya kompleks list komponen yang mengandung gambar. Untuk kasus seperti itu, langsung pake Fast Image saja.

Optimize 3rd UI Party Library

Hati-hati dalam memilih library, khususnya untuk UI component library. Ketika kita tahu bahwa suatu library menyebbakan performance drop, kita harus mengganti library itu atau mengoptimize librarynya dengan melakukan patching. Misalkan, jika komponen Pressable di library CatDesign itu memiliki waktu render 20ms, kita bisa ganti dengan Pressable bawaan React Native yang hanya render 3ms.

Prevent Redux Subscription untuk Komponen yang Tidak Terlihat

Ini sebenarnya adalah issue re-rendering yang tidak perlu yang terjadi ketika kita menggunakan global state managemen seperti Redux. Anggaplah kita punya 4 screen yang di push dalam suatu Stack. Screen Home -> ViewNotes -> Add Notes -> SuccessNotes adalah isi dari stack tersebut secara berutan. Ketika kita berada di SuccessNotes, kita mengupdate suatu global state yang menyebabkan Home ikut terender meski data yang diupdate tidak berhubungan. Kita bisa solve ini dengan cara memberi callback equality function pada saat get data di Home-nya.

State Optimization

Teknik ini maksudnya adalah mencegah re-render dengan cara membagi state yang tidak diperlukan dalam suatu komponen. Jika suatu komponen tidak memerlukan state/props, jangan buat dia re-render.

Contohnya anggaplah ada suatu komponen House.

Problem dari komponen di atas adalah Window selalu terender ketika state isOpen berubah. State isOpen sebenarnya hanya di butuhkan oleh Door saja. Kita bisa optimalkan dengan cara menempatkan state di dalam komponen Door (sebelumnya ada di komponen Home).

Dengan begini, hanya komponen Door saja yang terender ketika kita update state isOpen.

Contoh seperti ini sangat umum terjadi di aplikasi-aplikasi React based dan dapat dianalisa seberapa banyak ini terjadi di aplikasi kita.

Unsubscribe Listener

Ini merupakan teknik yang terinspirasi dari Bapak Krzysztof Magiera (Director Software Mansion) dalam tulisannya berjudul “Hunting JS memory leaks in React Native apps”. Dalam tulisan itu dikatakan bahwa penyebab memory leaks di javascript adalah unreleased/unsubscribed listener yang menjadi nomor 1 penyebab memory leaks berdasarkan pengalaman beliau.

Kita bisa lihat dalam snipped kode tersebut bahwa kita meregister listener keyboard namun belum ada mekanisme clear subscriptionnya. Nah, ketika kita meremove komponen itu maka listener ini tetap aktif. Bayangkan, listener ini tetep jalan padahal kita sudah tidak butuh. Masalah ini bisa diselesaikan dengan memanggil metod remove pada saat komponen di destroy

React Query

Ini merupakan optimasi API Call karena ia support caching, memoize query result dan works well dengan React Hooks. Ini merupakan opinionated library untuk data fetching di React yang penggunaannya cukup mudah. Contoh penggunaan:

Batching redux state

Teknik ini untuk mencegah multiple dispatching redux. Misal anggaplah ketika kita call suatu API GET /notes dan kita mendapatkan data berikut

Lalu kita akan menyimpan data tersebut dalam state yang berbeda

dispatch({type: ‘updateNotes’, data: notes.notes});dispatch({type: ‘updateTags’, data: notes.additionalInfo.availableTags});dispatch({type: ‘updateCategories’, data: notes.additionalInfo.availableCategories});

Ini akan membuat komponen terender sebanyak 3 kali. Kita bisa optimalkan dengan menggunakan batch supaya dia bisa update sekali render saja.

batch (() => {dispatch({type: ‘notes’, data: notes.notes});dispatch({type: ‘updateTags’, data: notes.additionalInfo.availableTags});dispatch({type: ‘updateCategories’, data: notes.additionalInfo.availableCategories});})

Menggunakan C++ via JSI

Jika kita punya kode yang melakukan heavy computation seperti dalam kriptografi, lebih baik functionnya ditulis di dalam C++. NodeJS tentunya tidak terlalu hebat dibandingkan C++ dalam hal memory consumption, karena memang C++ adalah high performance programming language. Ketika kita menulis kode di C++ kita bisa panggil functionnya via JSI (Javascript Interface). Salah satu contoh yang seperti ini ada pada library react-native-mmkv. Perbedaannya bisa 30 kali lipat lebih cepat.

Nah itu saja tentang cara mengukur dan beberapa teknik yang bisa dilakukan untuk optimalisasi performance di React Native application. Harapannya adalah kita sebagai developer bisa memberikan best result kepada user sehingga mereka bisa lebih happy memakai produk yang kita buat.

--

--

No responses yet