Pernahkah anda membina tooltip yang melompat dari sudut atas-kiri ke kedudukan sebenarnya? Atau modal yang berkelip pada saiz yang salah sebelum menetap di tempatnya? Gangguan sesaat itu adalah kelipan susun atur (layout flicker). Ia berlaku apabila React membaca DOM, mengira pembetulan, dan mengemas kini state, tetapi pelayar telah pun mula memaparkan piksel ke skrin. Preskripsi biasa adalah dengan menukar useEffect kepada useLayoutEffect. Pertukaran ini berkesan, tetapi hanya jika anda memahami dengan tepat bila setiap hook dijalankan di dalam aliran kerja pelayar.
Aliran Kerja Pelayar: Render, Commit, Paint
React mengemas kini komponen dalam tiga peringkat yang berbeza. Dalam fasa render, React membina—atau membina semula—Virtual DOM dan mengira perbezaan (diff). Belum ada perubahan piksel yang sebenar; ini hanyalah pengiraan tulen yang berlaku dalam memori. Seterusnya ialah fasa commit, di mana React menerapkan perubahan tersebut kepada nod DOM yang sebenar. Gaya (styles) dikemas kini, nod dimasukkan atau dibuang, dan teks berubah.
Kemudian pelayar mengambil alih. Dalam fasa paint, enjin rendering pelayar mengira geometri susun atur dan melukis piksel ke skrin. Urutan ini adalah tetap. Pelayar mesti melengkapkan susun atur sebelum ia boleh melukis (paint), dan ia mesti selesai melukis sebelum pengguna melihat apa-apa yang baharu. Jurang antara commit dan paint diukur dalam milisaat, tetapi ia benar-benar wujud, dan ia adalah ruang di mana useEffect dan useLayoutEffect berbeza.
Mengapa useEffect Menyebabkan Kelipan
useEffect berjalan secara asinkronus, dijadualkan untuk dijalankan selepas pelayar telah pun melukis (paint) skrin. DOM telah dikemas kini, piksel telah dilukis, dan kemudian React masuk semula untuk menjalankan effect anda.
Bayangkan anda merender menu dropdown di bawah sebuah butang. Di dalam useEffect, anda memanggil buttonRef.current.getBoundingClientRect(), mengira koordinat atas dan kiri yang betul, dan menyimpannya dalam state. Oleh kerana useEffect berjalan selepas paint, pelayar telah pun melukis dropdown pada kedudukan lalainya, mungkin pada top: 0, left: 0. Hanya selepas paint tersebut effect anda mengemas kini state. React melakukan commit pada koordinat yang telah dibetulkan, dan pelayar melukis semula. Pengguna melihat dua bingkai (frames): kedudukan yang salah, kemudian kedudukan yang betul. Peralihan visual yang mengejut itu adalah kelipan yang cuba dielakkan oleh semua orang.
Untuk pengambilan data, panggilan API, penjejakan analitik, atau menetapkan pendengar acara (event listeners), kelewatan ini tidak menjadi masalah. Pengguna tidak peduli sama ada penjejak analitik dijalankan beberapa milisaat selepas paint. Malah, menangguhkan kerja bukan visual sehingga selepas paint dapat memastikan render awal kekal responsif. Tetapi untuk pembetulan yang bergantung pada susun atur, useEffect adalah terlalu lewat.
Bagaimana useLayoutEffect Menyekat Paint
useLayoutEffect berjalan secara sinkronus, sejurus selepas React mengubah DOM tetapi sebelum pelayar berpeluang mengira susun atur atau melukis piksel. Ia menyekat keseluruhan saluran paip paint.
Jika anda melakukan pengukuran dropdown yang sama di dalam useLayoutEffect, urutannya berubah. React melakukan commit pada kemas kini DOM awal, menjalankan layout effect anda, dan kemas kini state anda mencetuskan re-render secara sinkronus. React melakukan commit pada koordinat yang telah dibetulkan, dan hanya selepas itu pelayar melukis (paint). Pengguna melihat satu bingkai sahaja, yang sudah pun betul.
Tingkah laku yang menyekat ini adalah ciri utama sekaligus risiko. Oleh kerana useLayoutEffect menghalang pelayar daripada melukis sehingga ia selesai, sebarang pengiraan berat di dalamnya akan membekukan UI. Walaupun hanya beberapa puluh milisaat paint tersekat, ia akan terasa seperti gangguan (jank) kepada pengguna. Inilah sebabnya mengapa dokumentasi React secara eksplisit memberitahu anda untuk bermula dengan useEffect dan hanya beralih ke useLayoutEffect apabila anda benar-benar memerhatikan kelipan yang tidak boleh diterima.
Bila Perlu Menggunakan Setiap Hook
Kebanyakan logik anda sepatutnya berada dalam useEffect. Gunakannya untuk:
- Mengambil data daripada API
- Menetapkan langganan (subscriptions) atau pendengar acara (event listeners)
- Menghantar acara analitik
- Sebarang kesan sampingan (side effect) yang tidak membaca atau mengubah susun atur secara serta-merta
Simpan useLayoutEffect untuk operasi yang mesti membaca DOM dan menulis semula sebelum pengguna melihat bingkai tersebut:
- Mengukur dimensi elemen, seperti lebar, tinggi, atau kedudukan skrol
- Mengira koordinat untuk tooltip, popover, atau menu konteks
- Mencegah peralihan susun atur yang kelihatan apabila kedudukan visual bergantung pada geometri yang dirender
Jika anda tidak pasti mana satu yang perlu dipilih, gunakan useEffect sebagai lalai. Beralih ke useLayoutEffect hanya apabila anda menyedari ketidakstabilan visual. Peraturan ini sahaja sudah cukup untuk memastikan sebahagian besar aplikasi React berjalan dengan lancar.
Masalah Server-Side Rendering (Gotcha)
Jika anda menggunakan Next.js, Remix, atau mana-mana rangka kerja yang merender React pada pelayan, anda akan menerima amaran dengan useLayoutEffect. Oleh sebab pelayan tidak mempunyai DOM, hook tersebut tidak mempunyai apa-apa untuk diukur. React memberi amaran bahawa ia menjangkakan persekitaran pelayar tetapi tidak menemuinya. Semasa hidrasi, ketidakpadanan ini juga boleh menyebabkan pepijat halus kerana markup yang dirender pada pelayan dan render pertama yang dimaksudkan oleh klien mungkin berbeza.
Penyelesaian standard ialah hook isomorfik yang memilih kesan (effect) yang betul berdasarkan persekitaran:
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
Gunakan pembungkus (wrapper) ini dalam mana-mana komponen yang mesti mengukur nod DOM tetapi mungkin dilaksanakan semasa perenderaan pelayan. Ia mendiamkan amaran tersebut dan memastikan output pelayan anda konsisten.
Prestasi dan Amalan Terbaik
Memandangkan useLayoutEffect menyekat proses mengecat (painting), pastikan kandungan hook tersebut seringan mungkin. Baca nilai susun atur, kira pembetulan, dan tulis semula. Jangan ambil data, mencerakin (parse) objek besar, atau menjalankan algoritma yang berat di dalamnya. Kod yang berat di sini akan melambatkan thread utama dan menyebabkan antara muka anda terasa beku.
Apabila anda mengukur elemen, gunakan ref React berbanding document.getElementById. Ref terikat pada instans komponen anda, kekal semasa render semula tanpa helah pertanyaan (query tricks), dan berfungsi dengan dipercayai bersama portal atau perenderaan bersyarat. Carian ID global merosakkan enkapsulasi komponen dan boleh mengembalikan null tepat pada saat anda memerlukannya.
useEffect adalah pilihan lalai yang betul untuk hampir setiap kesan sampingan (side effect). Ia membolehkan pelayar mengecat tanpa gangguan dan mengendalikan data, acara, serta penyinkronan luaran dengan kemas. useLayoutEffect ialah alat khusus untuk masalah tertentu: membaca susun atur dan menulis semula sebelum proses mengecat. Kuasai perbezaan masa antara keduanya, dan anda akan berhenti mengejar kesan berkelip (flickers) dan mula mencegahnya.
Intipati sebenar: Mulakan dengan useEffect untuk segalanya. Sebaik sahaja anda melihat tooltip atau modal berkelip ke tempat yang salah sebelum membetulkan dirinya sendiri, itulah isyarat anda. Tukar kepada useLayoutEffect, ukur DOM, laras susun atur anda, dan biarkan pelayar mengecat sekali sahaja—dengan betul.
