Kebanyakan tutorial prestasi React berakhir dengan nasihat buruk yang sama: bungkus segala-galanya dalam useMemo dan useCallback dan anggap tugas selesai. Jika anda mengikut nasihat tersebut, anda mungkin telah menjadikan aplikasi anda lebih perlahan. Hook ini tidak percuma. Setiap satunya memperuntukkan memori, membandingkan kebergantungan (dependencies), dan menyimpan nilai yang telah dicache. Jika digunakan tanpa tujuan, ia menjadi beban (overhead) dan bukannya pengoptimuman.
Mari kita ringkaskan kepada apa yang sebenarnya penting.
Apa yang Sebenarnya Dilakukan oleh Setiap Hook
useMemo mengingati satu nilai. Anda memberikannya satu fungsi yang melakukan kerja berat, dan ia mengembalikan hasilnya. Pada render seterusnya, jika kebergantungan anda tidak berubah, React akan melangkau pengiraan tersebut dan mengembalikan hasil lama.
useCallback mengingati satu fungsi. Ia tidak menjalankan fungsi tersebut untuk anda. Ia hanya mengembalikan instans fungsi yang sama antara render selagi kebergantungannya kekal sama.
Itulah perbezaan keseluruhannya. Satu menyimpan cache nilai yang dikira. Satu lagi menyimpan cache rujukan (reference). Kekeliruan antara keduanya akan menghasilkan kod yang kelihatan dioptimumkan tetapi berkelakuan sama seperti kod tanpa pembungkusan, sambil menelan kos memori tambahan.
Mengapa Identiti Fungsi Merosakkan Tree Anda
Apabila sesuatu komponen melakukan render semula (re-render), React akan melaksanakan keseluruhan badan fungsi itu sekali lagi. Setiap pemboleh ubah dicipta semula. Setiap fungsi dalam talian (inline function) mendapat alamat baharu dalam memori.
Dalam JavaScript, dua fungsi yang mengandungi logik yang sama tepat tidak adalah sama. () => {} === () => {} akan menghasilkan nilai false. Peraturan yang sama terpakai kepada objek dan tatasusunan (arrays). Jika komponen induk anda mentakrifkan handleSubmit dan memberikannya kepada komponen anak, anak tersebut akan menerima prop baharu pada setiap render. Walaupun komponen anak dibungkus dalam React.memo, ia tidak dapat membezakan bahawa fungsi baharu itu melakukan perkara yang sama seperti fungsi lama. Rujukan telah berubah, jadi komponen anak akan melakukan render semula.
Inilah masalah utama yang dibina untuk diselesaikan oleh useCallback. Ia bukan tentang kelajuan. Ia adalah tentang kestabilan.
Bilakah useMemo Berbaloi Digunakan
Anda memerlukan useMemo apabila anda melakukan kerja yang secara objektifnya berat dan anda dapat melihat lengah (lag) yang boleh diukur.
Fikirkan tentang penapisan set data yang besar. Jika anda mempunyai jadual dengan berpuluh-puluh ribu baris dan input carian, anda mungkin menulis sesuatu seperti ini di dalam komponen anda:
const visibleRows = rows.filter(r => r.name.includes(query));
Tanpa useMemo, gelung (loop) tersebut akan berjalan pada setiap render. Jika pengguna mengklik butang yang menukar paparan sidebar, komponen induk akan melakukan render semula, dan penapis anda akan berjalan sekali lagi walaupun rows dan query tidak pernah berubah. Pada set data yang besar, gangguan (stutter) tersebut akan kelihatan.
useMemo membaiki perkara ini dengan menetapkan hasil tersebut:
const visibleRows = useMemo(() => {
return rows.filter(r => r.name.includes(query));
}, [rows, query]);
Kini React hanya menjalankan semula penapis tersebut apabila kebergantungan benar-benar berubah.
Logik yang sama terpakai kepada pengiraan matematik yang kompleks, mengubah respons API kepada format yang sesuai untuk carta, atau menderivasi keadaan (state) yang jika tidak, akan dikira semula secara berterusan.
Terdapat kes penggunaan kedua yang kurang jelas. Jika anda mencipta objek atau tatasusunan secara tempatan dan memasukkannya ke dalam tatasusunan kebergantungan useEffect, anda boleh secara tidak sengaja mencetuskan kesan (effect) tersebut pada setiap render. Objek dan tatasusunan dalam talian mendapat identiti baharu setiap kali, jadi kesan tersebut melihat kebergantungan yang telah berubah dan akan dicetuskan semula. Melakukan memoisasi pada objek tersebut dengan useMemo akan mengekalkan rujukan yang stabil dan membolehkan kesan anda berjalan hanya apabila data asas benar-benar berubah.
Bilakah useCallback Menjadi Perlu
useCallback paling penting apabila anda menghantar pengendali (handlers) ke dalam komponen anak yang telah dioptimumkan dengan React.memo.
Bayangkan sebuah komponen induk yang memegang satu pengira (counter). Ia juga melakukan render senarai anak yang berat:
function Parent() {
const [count, setCount] = useState(0);
const handleItemClick = (id) => {
console.log(id);
};
return (
<div>
<button onClick={() => setCount(c + 1)}>{count}</button>
<ExpensiveList onItemClick={handleItemClick} />
</div>
);
}
Setiap kali count berubah, Parent akan melakukan render semula. handleItemClick yang baharu akan dicipta. Oleh kerana ExpensiveList menerima rujukan prop yang baharu, ia juga akan melakukan render semula. Jika ExpensiveList dibungkus dalam React.memo, memoisasi tersebut akan menjadi sia-sia kerana prop fungsi telah berubah.
useCallback mengekalkan rujukan tersebut:
const handleItemClick = useCallback((id) => {
console.log(id);
}, []);
Kini ExpensiveList hanya melakukan render semula apabila ia benar-benar perlu.
Satu lagi situasi kritikal melibatkan useEffect. Jika sesuatu kesan melanggan (subscribes) kepada fungsi yang ditakrifkan di dalam komponen anda, dan fungsi tersebut berubah identiti pada setiap render, kesan tersebut akan dinyahaktifkan (teardown) dan melanggan semula secara berulang kali. Melakukan memoisasi pada fungsi tersebut akan mengekalkan kestabilan kesan tersebut.
Perangkap Tatasusunan Kebergantungan dan Stale Closures
Kedua-dua hook bergantung pada tatasusunan kebergantungan, dan di sinilah kebanyakan pepijat (bugs) bersembunyi.
Jika anda meninggalkan pemboleh ubah daripada tatasusunan kebergantungan (dependency array), fungsi atau nilai yang telah dimemoisasikan akan merujuk kepada versi lama pemboleh ubah tersebut. Ini adalah stale closure. UI mungkin memaparkan data terkini, tetapi callback anda masih melihat state daripada tiga kali render yang lalu. Penyelesaiannya mudah tetapi mudah terlepas pandang semasa semakan kod: sertakan setiap nilai yang digunakan di dalam hook yang boleh berubah.
Jalankan peraturan ESLint react-hooks/exhaustive-deps. Ia akan mengesan ketinggalan yang ketara. Namun, jangan anggap ia sebagai robot. Fahami mengapa setiap kebergantungan itu penting.
Cukai Tersembunyi Akibat Pengoptimuman Berlebihan
Pemula sering membentengi setiap fungsi dan setiap nilai dengan hook ini kerana ia terasa selamat. Tabiat tersebut akan memakan diri.
React mesti menyimpan nilai yang telah dicache dalam memori. Pada setiap render, ia mesti menyemak tatasusunan kebergantungan anda dan membandingkan setiap item menggunakan Object.is. Perbandingan itu murah, tetapi ia tidak percuma. Jika anda membungkus pengendali acara (event handler) yang remeh seperti onClick={() => setOpen(true)} di dalam useCallback, anda sebenarnya membayar kos memori dan CPU hanya untuk mengelakkan penciptaan fungsi yang sepatutnya boleh diperuntukkan secara serta-merta.
Hook tersebut juga menambah kekusutan (noise). Kod yang dibungkus dalam useMemo dan useCallback lebih sukar dibaca dan lebih sukar untuk diselenggara. Setiap tatasusunan kebergantungan adalah potensi stale closure yang menunggu masa untuk menyusahkan anda.
Peraturan praktikal yang sebenar adalah tidak menarik tetapi berkesan: tulis kod biasa terlebih dahulu. Optimumkan hanya apabila anda mempunyai bukti masalah. Gunakan React DevTools Profiler untuk mengenal pasti komponen mana yang mahal dan render mana yang membazir. Jika satu render mengambil masa kurang daripada beberapa milisaat, tiada pengguna akan menyedarinya, dan memoisasi anda tidak menyelesaikan apa-apa.
Kesimpulan
useMemo adalah untuk nilai yang mahal. useCallback adalah untuk rujukan fungsi yang stabil. Kedua-dua hook ini tidak menjadikan render komponen anda lebih pantas dengan sendirinya; ia menghalang kerja hiliran (downstream) yang tidak perlu. Mulakan tanpa mereka, ukur dengan alatan sebenar, dan tambahkan mereka tepat di mana profiler menunjukkan bottleneck. Kod bersih yang melakukan render semula sekali-sekala hampir sentiasa lebih baik daripada kod yang direka secara berlebihan (over-engineered) yang melakukan memoisasi pada segalanya.
