Memahami Perilaku Rendering di React
Diterbitkan: 2020-11-16Seiring dengan kehidupan, kematian, nasib, dan pajak, perilaku rendering React adalah salah satu kebenaran dan misteri terbesar dalam hidup.
Mari selami!
Seperti orang lain, saya memulai perjalanan pengembangan front-end saya dengan jQuery. Manipulasi DOM murni berbasis JS adalah mimpi buruk saat itu, jadi itulah yang dilakukan semua orang. Kemudian perlahan-lahan, kerangka kerja berbasis JavaScript menjadi begitu menonjol sehingga saya tidak bisa mengabaikannya lagi.
Yang pertama saya pelajari adalah Vue. Saya mengalami waktu yang sangat sulit karena komponen dan keadaan dan yang lainnya adalah model mental yang sama sekali baru, dan sangat sulit untuk menyesuaikan semuanya. Tapi akhirnya, saya melakukannya, dan menepuk punggung saya sendiri. Selamat, sobat, saya berkata pada diri sendiri, Anda telah melakukan pendakian yang curam; sekarang, kerangka kerja lainnya, jika Anda perlu mempelajarinya, akan sangat mudah.
Jadi, suatu hari, ketika saya mulai belajar React, saya menyadari betapa salahnya saya. Facebook tidak membuat segalanya lebih mudah dengan melemparkan Hooks dan memberi tahu semua orang, “Hei, gunakan ini mulai sekarang. Tapi jangan menulis ulang kelas; kelas baik-baik saja. Sebenarnya tidak begitu banyak, tapi tidak apa-apa. Tapi Hooks adalah segalanya, dan mereka adalah masa depan.
Mengerti? Besar!".
Akhirnya, saya juga melintasi gunung itu. Tapi kemudian saya terkena sesuatu yang sama pentingnya dan sulitnya dengan React itu sendiri: rendering .

Jika Anda telah menemukan rendering dan misterinya di React, Anda tahu apa yang saya bicarakan. Dan jika belum, Anda tidak tahu apa yang akan terjadi pada Anda!
Tetapi sebelum membuang waktu untuk apa pun, adalah kebiasaan yang baik untuk menanyakan apa yang akan Anda peroleh darinya (tidak seperti saya, yang idiot yang terlalu bersemangat dan dengan senang hati akan mempelajari apa pun hanya untuk kepentingan itu). Jika hidup Anda sebagai pengembang React berjalan dengan baik tanpa khawatir tentang apa rendering ini, mengapa peduli? Pertanyaan yang bagus, jadi mari kita jawab ini dulu, dan kemudian kita akan melihat apa sebenarnya rendering itu.
Mengapa memahami perilaku rendering di React itu penting?
Kita semua mulai belajar React dengan menulis (hari ini, fungsional) komponen yang mengembalikan sesuatu yang disebut JSX. Kami juga memahami bahwa JSX ini entah bagaimana diubah menjadi elemen DOM HTML aktual yang muncul di halaman. Halaman diperbarui saat status diperbarui, rute berubah seperti yang diharapkan, dan semuanya baik-baik saja. Tetapi pandangan tentang cara kerja React ini naif dan merupakan sumber dari banyak masalah.
Meskipun kami sering berhasil menulis aplikasi berbasis React yang lengkap, ada kalanya kami menemukan bagian tertentu dari aplikasi kami (atau seluruh aplikasi) sangat lambat. Dan bagian terburuknya. . . kami tidak memiliki satu petunjuk pun mengapa! Kami telah melakukan semuanya dengan benar, kami tidak melihat kesalahan atau peringatan, kami telah mengikuti semua praktik yang baik dari desain komponen, standar pengkodean, dll., dan tidak ada kelambatan jaringan atau komputasi logika bisnis yang mahal yang terjadi di belakang layar.
Terkadang, ini adalah masalah yang sama sekali berbeda: tidak ada yang salah dengan kinerjanya, tetapi aplikasi berperilaku aneh. Misalnya, membuat tiga panggilan API ke backend autentikasi tetapi hanya satu ke yang lainnya. Atau beberapa halaman digambar ulang dua kali, dengan transisi yang terlihat antara dua render halaman yang sama menciptakan UX yang menggelegar.

Yang terburuk, tidak ada bantuan eksternal yang tersedia dalam kasus seperti ini. Jika Anda pergi ke forum pengembang favorit Anda dan menanyakan pertanyaan ini, mereka akan menjawab, “Tidak tahu tanpa melihat aplikasi Anda. Bisakah Anda melampirkan contoh kerja minimum di sini? ” Yah, Anda, tentu saja, tidak dapat melampirkan seluruh aplikasi karena alasan hukum, sementara contoh kerja kecil dari bagian itu mungkin tidak mengandung masalah itu karena tidak berinteraksi dengan seluruh sistem seperti di aplikasi yang sebenarnya.
Kacau? Ya, jika Anda bertanya kepada saya.
Jadi, kecuali jika Anda ingin melihat hari-hari sengsara seperti itu, saya sarankan Anda mengembangkan pemahaman — dan minat, saya harus bersikeras; pemahaman yang diperoleh dengan enggan tidak akan membawa Anda jauh di dunia React — dalam hal yang kurang dipahami ini disebut rendering di React. Percayalah, itu tidak sulit untuk dipahami, dan meskipun sangat sulit untuk dikuasai, Anda akan melangkah sangat jauh tanpa harus mengetahui setiap sudut dan celah.
Apa yang dimaksud dengan rendering di React?
Itu, teman saya, adalah pertanyaan yang bagus. Kita cenderung tidak menanyakannya saat mempelajari React (saya tahu karena saya tidak melakukannya) karena kata "render" mungkin membuat kita terbuai dengan rasa keakraban yang salah. Meskipun arti kamus benar-benar berbeda (dan itu tidak penting dalam diskusi ini), kami para programmer sudah memiliki gagasan tentang apa artinya. Bekerja dengan layar, API 3D, kartu grafis, dan membaca spesifikasi produk melatih pikiran kita untuk memikirkan sesuatu di sepanjang baris "melukis gambar" ketika kita membaca kata "render". Dalam pemrograman game-engine, ada Renderer, yang tugasnya adalah — tepatnya!, melukis dunia seperti yang diserahkan oleh Scene.
Jadi kami berpikir bahwa ketika React "merender" sesuatu, ia mengumpulkan semua komponen dan mengecat ulang DOM halaman web. Tetapi di dunia React (dan ya, bahkan dalam dokumentasi resmi), bukan itu yang dimaksud dengan rendering. Jadi, mari kencangkan sabuk pengaman kita dan menyelami bagian dalam React secara mendalam.

Anda pasti pernah mendengar bahwa React memelihara apa yang disebut DOM virtual dan secara berkala membandingkannya dengan DOM aktual dan menerapkan perubahan seperlunya (inilah mengapa Anda tidak bisa memasukkan jQuery dan React bersama-sama — React perlu mengambil kendali penuh atas DOM). Sekarang, DOM virtual ini tidak terdiri dari elemen HTML seperti DOM asli, tetapi dari elemen React. Apa bedanya? Pertanyaan bagus! Mengapa tidak membuat aplikasi React kecil dan melihatnya sendiri?
Saya membuat aplikasi React yang sangat sederhana ini untuk tujuan ini. Seluruh kode hanyalah satu file yang berisi beberapa baris:
import React from "react"; import "./styles.css"; export default function App() { const element = ( <div className="App"> <h1>Hello, there!</h1> <h2>Let's take a look inside React elements</h2> </div> ); console.log(element); return element; }
Perhatikan apa yang kita lakukan di sini?
Ya, cukup catat seperti apa elemen JSX. Ekspresi dan komponen BEJ ini adalah sesuatu yang telah kami tulis ratusan kali, tetapi kami jarang memperhatikan apa yang terjadi. Jika Anda membuka konsol pengembang browser dan menjalankan aplikasi ini, Anda akan melihat Object
yang meluas ke:

Ini mungkin terlihat menakutkan, tetapi perhatikan beberapa detail menarik:
- Apa yang kami lihat adalah objek JavaScript biasa dan biasa dan bukan simpul DOM.
- Perhatikan bahwa properti
props
mengatakan bahwa ia memilikiclassName
dariApp
(yang merupakan kelas CSS yang diatur dalam kode) dan elemen ini memiliki dua anak (ini juga cocok, elemen anak menjadi<h1>
dan<h2>
) . - Properti
_source
memberi tahu kita di mana kode sumber memulai tubuh elemen. Seperti yang Anda lihat, ia menamai fileApp.js
sebagai sumber dan menyebutkan nomor baris 6. Jika Anda melihat kodenya lagi, Anda akan menemukan bahwa baris 6 tepat setelah tag JSX pembuka, yang masuk akal. Tanda kurung JSX berisi elemen React; mereka bukan bagian darinya, karena mereka berfungsi untuk berubah menjadi panggilanReact.createElement()
nanti. - Properti
__proto__
memberitahu kita bahwa objek ini mendapatkan semua miliknya. properties dari root JavaScriptObject
, sekali lagi memperkuat gagasan bahwa itu hanya objek JavaScript sehari-hari yang kita lihat di sini.
Jadi, sekarang, kami memahami bahwa apa yang disebut DOM virtual tidak terlihat seperti DOM asli tetapi merupakan pohon objek React (JavaScript) yang mewakili UI pada saat itu.

Lelah?
Percayalah, aku juga. Memutar ide-ide ini berulang-ulang di kepala saya untuk mencoba dan menyajikannya dengan cara terbaik, dan kemudian memikirkan kata-kata untuk mengeluarkannya dan mengaturnya kembali — tidak mudah.
Tapi kita terganggu!
Setelah bertahan sejauh ini, kami sekarang dalam posisi untuk menjawab pertanyaan yang kami cari: apa yang dirender di React?
Nah, rendering adalah proses mesin React yang berjalan melalui DOM virtual dan mengumpulkan status saat ini, props, struktur, perubahan yang diinginkan di UI, dll. React sekarang memperbarui DOM virtual menggunakan beberapa perhitungan dan juga membandingkan hasil baru dengan DOM yang sebenarnya di halaman. Penghitungan dan perbandingan inilah yang secara resmi disebut oleh tim React sebagai “rekonsiliasi”, dan jika Anda tertarik dengan ide dan algoritme yang relevan, Anda dapat memeriksa dokumen resminya.
Saatnya Berkomitmen!
Setelah bagian rendering selesai, React memulai fase yang disebut "commit", di mana ia menerapkan perubahan yang diperlukan ke DOM. Perubahan ini diterapkan secara sinkron (satu demi satu, meskipun mode baru yang bekerja secara bersamaan diharapkan segera), dan DOM diperbarui. Kapan tepatnya dan bagaimana React menerapkan perubahan ini bukanlah urusan kami, karena ini adalah sesuatu yang sepenuhnya tersembunyi dan kemungkinan akan terus berubah saat tim React mencoba hal-hal baru.
Rendering dan performa di aplikasi React
Kami telah memahami sekarang bahwa rendering berarti mengumpulkan info, dan tidak perlu menghasilkan perubahan DOM visual setiap saat. Kami juga tahu bahwa apa yang kami anggap sebagai "rendering" adalah proses dua langkah yang melibatkan rendering dan commit. Sekarang kita akan melihat bagaimana rendering (dan yang lebih penting, rendering ulang) dipicu di aplikasi React dan bagaimana tidak mengetahui detailnya dapat menyebabkan aplikasi berkinerja buruk.
Re-render karena perubahan komponen induk
Jika komponen induk di React berubah (katakanlah, karena status atau propsnya berubah), React menelusuri seluruh pohon di bawah elemen induk ini dan merender ulang semua komponen. Jika aplikasi Anda memiliki banyak komponen bersarang dan banyak interaksi, Anda secara tidak sadar menerima pukulan kinerja besar setiap kali Anda mengubah komponen induk (dengan asumsi itu hanya komponen induk yang ingin Anda ubah).

Benar, rendering tidak akan menyebabkan React mengubah DOM yang sebenarnya karena, selama rekonsiliasi, ia akan mendeteksi bahwa tidak ada yang berubah untuk komponen ini. Tapi, itu masih waktu CPU dan memori yang terbuang, dan Anda akan terkejut betapa cepatnya itu bertambah.
Re-render karena perubahan Konteks
Fitur React's Context tampaknya menjadi alat manajemen negara favorit semua orang (sesuatu yang sama sekali tidak dibuat untuknya). Semuanya sangat nyaman — cukup bungkus komponen paling atas di penyedia konteks, dan sisanya adalah masalah sederhana! Sebagian besar aplikasi React dibuat seperti ini, tetapi jika Anda telah membaca artikel ini sejauh ini, Anda mungkin telah melihat apa yang salah. Ya, setiap kali objek konteks diperbarui, itu memicu rendering ulang besar-besaran dari semua komponen pohon.
Sebagian besar aplikasi tidak memiliki kesadaran kinerja, jadi tidak ada yang memperhatikan, tetapi seperti yang dikatakan sebelumnya, kelalaian seperti itu bisa sangat mahal dalam aplikasi volume tinggi dan interaksi tinggi.
Meningkatkan kinerja rendering React
Jadi, dengan semua ini, apa yang dapat kita lakukan untuk meningkatkan kinerja aplikasi kita? Ternyata ada beberapa hal yang bisa kita lakukan, tetapi perhatikan bahwa kita hanya akan membahas dalam konteks komponen fungsional. Komponen berbasis kelas sangat tidak disarankan oleh tim React dan sedang dalam perjalanan keluar.
Gunakan Redux atau perpustakaan serupa untuk manajemen negara
Mereka yang menyukai dunia Konteks yang cepat dan kotor cenderung membenci Redux, tetapi hal ini sangat populer untuk alasan yang baik. Dan salah satu alasannya adalah kinerja — fungsi connect()
di Redux ajaib karena (hampir selalu) hanya merender komponen-komponen itu jika diperlukan. Ya, ikuti saja arsitektur Redux standar, dan kinerjanya gratis. Sama sekali tidak berlebihan jika Anda mengadopsi arsitektur Redux, Anda langsung menghindari sebagian besar masalah kinerja (dan lainnya).
Gunakan memo()
untuk "membekukan" komponen
Nama "memo" berasal dari Memoization, yang merupakan nama mewah untuk caching. Dan jika Anda tidak menemukan banyak caching, tidak apa-apa; inilah deskripsi yang dipermudah: setiap kali Anda membutuhkan hasil perhitungan/operasi, Anda mencari di tempat di mana Anda telah mempertahankan hasil sebelumnya; jika Anda menemukannya, bagus, cukup kembalikan hasil itu; jika tidak, lanjutkan dan lakukan operasi/komputasi itu.
Sebelum masuk langsung ke memo()
, mari kita lihat dulu bagaimana rendering yang tidak perlu terjadi di React. Kami mulai dengan skenario langsung: bagian kecil dari UI aplikasi yang menunjukkan kepada pengguna berapa kali mereka menyukai layanan/produk (jika Anda mengalami kesulitan menerima kasus penggunaan, pikirkan bagaimana di Medium Anda dapat "bertepuk tangan". ” beberapa kali untuk menunjukkan seberapa besar Anda mendukung/menyukai sebuah artikel).
Ada juga tombol yang memungkinkan mereka menambah suka sebanyak 1. Dan terakhir, ada komponen lain di dalamnya yang menunjukkan detail akun dasar kepada pengguna. Jangan khawatir sama sekali jika Anda merasa ini sulit untuk diikuti; Sekarang saya akan memberikan kode langkah demi langkah untuk semuanya (dan tidak banyak), dan pada akhirnya, tautan ke taman bermain tempat Anda dapat mengacaukan aplikasi yang berfungsi dan meningkatkan pemahaman Anda.
Mari kita pertama menangani komponen tentang info pelanggan. Mari kita buat file bernama CustomerInfo.js
yang berisi kode berikut:
import React from "react"; export const CustomerInfo = () => { console.log("CustomerInfo was rendered! :O"); return ( <React.Fragment> <p>Name: Sam Punia</p> <p>Email: [email protected]</p> <p>Preferred method: Online</p> </React.Fragment> ); };
Tidak ada yang mewah, kan?
Hanya beberapa teks informasi (yang bisa saja melewati alat peraga) yang tidak diharapkan berubah saat pengguna berinteraksi dengan aplikasi (untuk puritan di luar sana, ya, tentu saja itu bisa berubah, tetapi intinya adalah, jika dibandingkan dengan yang lain aplikasi, itu praktis statis). Tapi perhatikan pernyataan console.log()
. Ini akan menjadi petunjuk kami untuk mengetahui bahwa komponen telah dirender (ingat, "dirender" berarti infonya dikumpulkan dan dihitung/dibandingkan, dan bukan karena itu dilukis ke DOM yang sebenarnya).
Jadi, selama pengujian kami, jika kami tidak melihat pesan seperti itu di konsol browser, komponen kami tidak dirender sama sekali; jika kita melihatnya muncul 10 kali, itu berarti komponen itu dirender 10 kali; dan seterusnya.
Dan sekarang mari kita lihat bagaimana komponen utama kita menggunakan komponen info pelanggan ini:
import React, { useState } from "react"; import "./styles.css"; import { CustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <CustomerInfo /> </div> </div> ); }
Jadi, kita melihat bahwa komponen App
memiliki status internal yang dikelola melalui kait useState()
. Status ini terus menghitung berapa kali pengguna menyukai layanan/situs, dan awalnya disetel ke nol. Tidak ada yang menantang sejauh aplikasi React berjalan, bukan? Di sisi UI, semuanya terlihat seperti ini:

Tombolnya terlihat terlalu menggoda untuk tidak dihancurkan, setidaknya bagi saya! Tetapi sebelum saya melakukannya, saya akan membuka konsol pengembang browser saya dan menghapusnya. Setelah itu, saya akan menekan tombol beberapa kali, dan inilah yang saya lihat:

Saya telah menekan tombol 19 kali, dan seperti yang diharapkan, jumlah suka total mencapai 19. Skema warna membuatnya sangat sulit untuk dibaca, jadi saya menambahkan kotak merah untuk menyorot hal utama: komponen <CustomerInfo />
telah diberikan 20 kali!
Mengapa 20?
Sekali ketika semuanya awalnya dirender, dan kemudian, 19 kali ketika tombol ditekan. Tombol mengubah totalLikes
, yang merupakan bagian dari status di dalam komponen <App />
, dan sebagai hasilnya, komponen utama dirender ulang. Dan seperti yang telah kita pelajari di bagian awal posting ini, semua komponen di dalamnya juga dirender ulang. Ini tidak diinginkan karena komponen <CustomerInfo />
tidak berubah dalam proses dan belum berkontribusi pada proses rendering.
Bagaimana kita bisa mencegahnya?
Persis seperti judul bagian ini, menggunakan fungsi memo()
untuk membuat salinan komponen <CustomerInfo />
yang "dipertahankan" atau disimpan dalam cache. Dengan komponen memo, React melihat propsnya dan membandingkannya dengan props sebelumnya, dan jika tidak ada perubahan, React tidak mengekstrak output "render" baru dari komponen ini.
Mari tambahkan baris kode ini ke file CustomerInfo.js
kita:
export const MemoizedCustomerInfo = React.memo(CustomerInfo);
Ya, hanya itu yang perlu kita lakukan! Sekarang saatnya untuk menggunakan ini di komponen utama kita dan lihat apakah ada perubahan:
import React, { useState } from "react"; import "./styles.css"; import { MemoizedCustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <MemoizedCustomerInfo /> </div> </div> ); }
Ya, hanya dua baris yang berubah, tetapi saya tetap ingin menampilkan seluruh komponen. Tidak ada yang mengubah UI-bijaksana, jadi jika saya mengambil versi baru untuk berputar dan menghancurkan tombol suka beberapa kali, saya mendapatkan ini:

Jadi, berapa banyak pesan konsol yang kita miliki?
Hanya satu! Artinya selain render awal, komponen tidak tersentuh sama sekali. Bayangkan peningkatan kinerja pada aplikasi berskala sangat tinggi! Oke, oke, tautan ke taman bermain kode yang saya janjikan ada di sini. Untuk mereplikasi contoh sebelumnya, Anda harus mengimpor dan menggunakan CustomerInfo
alih-alih MemoizedCustomerInfo
dari CustomerInfo.js
.
Yang mengatakan, memo()
bukanlah pasir ajaib yang dapat Anda taburkan di mana-mana dan mengharapkan hasil yang ajaib. Terlalu sering menggunakan memo()
juga dapat menimbulkan bug rumit di aplikasi Anda dan, terkadang, hanya menyebabkan beberapa pembaruan yang diharapkan gagal. Saran umum tentang pengoptimalan "prematur" juga berlaku di sini. Pertama, bangun aplikasi Anda seperti yang dikatakan intuisi Anda; kemudian, lakukan beberapa pembuatan profil intensif untuk melihat bagian mana yang lambat dan jika tampaknya komponen memoized adalah solusi yang tepat, baru kemudian perkenalkan ini.
Desain komponen "Cerdas"
Saya menempatkan "cerdas" dalam tanda kutip karena: 1) Kecerdasan sangat subjektif dan situasional; 2) Seharusnya tindakan cerdas sering memiliki konsekuensi yang tidak menyenangkan. Jadi, saran saya untuk bagian ini adalah: jangan terlalu percaya diri dengan apa yang Anda lakukan.
Dengan mengesampingkan hal itu, satu kemungkinan untuk meningkatkan kinerja rendering adalah merancang dan menempatkan komponen sedikit berbeda. Misalnya, komponen anak dapat di-refactored dan dipindahkan ke suatu tempat di hierarki untuk menghindari rendering ulang. Tidak ada aturan yang mengatakan, "komponen ChatPhotoView harus selalu berada di dalam komponen Obrolan". Dalam kasus khusus (dan ini adalah kasus di mana kami memiliki bukti yang didukung data bahwa kinerja terpengaruh), menekuk/melanggar aturan sebenarnya bisa menjadi ide bagus.
Kesimpulan
Banyak lagi yang bisa dilakukan untuk mengoptimalkan aplikasi React secara umum, tetapi karena artikel ini tentang Rendering, saya membatasi ruang lingkup diskusi. Terlepas dari itu, saya harap Anda sekarang memiliki wawasan yang lebih baik tentang apa yang terjadi di React di bawah tenda, apa sebenarnya rendering, dan bagaimana hal itu dapat memengaruhi kinerja aplikasi.
Selanjutnya, mari kita pahami apa itu React Hooks?