
Pernahkah Anda mendengar tentang "mengangkat status"? Saya kira Anda punya dan itulah alasan mengapa Anda ada di sini. Bagaimana mungkin salah satu dari 12 konsep utama yang tercantum dalam Bereaksi dokumentasi resmi dapat menyebabkan kinerja yang buruk? Dalam artikel ini, kami akan mempertimbangkan situasi yang memang benar terjadi.
Langkah 1: angkat
Saya sarankan Anda untuk membuat gim tic-tac-toe sederhana. Untuk permainan, kita perlu:
Beberapa kondisi permainan. Tidak ada logika permainan nyata untuk mengetahui apakah kami menang atau kalah. Hanya array dua dimensi sederhana yang diisi dengan "x"
atau "0".
tidak undefined
"0".
const size = 10
Wadah induk untuk menampung keadaan permainan kami.
const App = () => { const [field, setField] = useState(initialField) return ( <div> {field.map((row, rowI) => ( <div> {row.map((cell, cellI) => ( <Cell content={cell} setContent={ // Update a single cell of a two-dimensional array // and return a new two dimensional array (newContent) => setField([ // Copy rows before our target row ...field.slice(0, rowI), [ // Copy cells before our target cell ...field[rowI].slice(0, cellI), newContent, // Copy cells after our target cell ...field[rowI].slice(cellI + 1), ], // Copy rows after our target row ...field.slice(rowI + 1), ]) } /> ))} </div> ))} </div> ) }
Komponen anak untuk menampilkan keadaan sel tunggal.
const randomContent = () => (Math.random() > 0.5 ? 'x' : '0') const Cell = ({ content, setContent }) => ( <div onClick={() => setContent(randomContent())}>{content}</div> )
Demo langsung # 1
Sejauh ini terlihat bagus. Bidang yang sangat reaktif yang dapat Anda berinteraksi dengan kecepatan cahaya :) Ayo tambah ukurannya. Katakan, ke 100. Ya, saatnya untuk mengklik tautan demo itu dan mengubah variabel size
di bagian paling atas. Masih cepat untukmu? Coba 200 atau gunakan pelambatan CPU yang terpasang di Chrome . Apakah Anda sekarang melihat jeda yang signifikan antara waktu Anda mengklik sel dan waktu kontennya berubah?
Mari ubah size
kembali menjadi 10 dan tambahkan beberapa profil untuk menyelidiki penyebabnya.
const Cell = ({ content, setContent }) => { console.log('cell rendered') return <div onClick={() => setContent(randomContent())}>{content}</div> }
Demo langsung # 2
Ya itu saja. console.log
sederhana akan cukup karena berjalan di setiap render.
Jadi apa yang kita lihat? Berdasarkan angka pada pernyataan "yang direnderkan sel" (untuk size
= N seharusnya N) di konsol kami, sepertinya seluruh bidang dirender ulang setiap kali sel tunggal berubah.
Hal yang paling jelas untuk dilakukan adalah menambahkan beberapa kunci seperti yang disarankan oleh React .
<div> {field.map((row, rowI) => ( <div key={rowI}> {row.map((cell, cellI) => ( <Cell key={`row${rowI}cell${cellI}`} content={cell} setContent={(newContent) => setField([ ...field.slice(0, rowI), [ ...field[rowI].slice(0, cellI), newContent, ...field[rowI].slice(cellI + 1), ], ...field.slice(rowI + 1), ]) } /> ))} </div> ))} </div>
Demo langsung # 3
Namun, setelah menambah size
lagi kita melihat bahwa masalah itu masih ada. Kalau saja kita bisa melihat mengapa komponen apa pun merender ... Untungnya, kita dapat dengan bantuan dari React DevTools yang luar biasa. Itu mampu merekam mengapa komponen bisa dirender. Anda harus mengaktifkannya secara manual.

Setelah diaktifkan, kita dapat melihat bahwa semua sel dirender ulang karena alat peraga mereka berubah, khususnya, setContent
prop.

Setiap sel memiliki dua alat bantu: content
dan setContent
. Jika sel [0] [0] berubah, konten sel [0] [1] tidak berubah. Di sisi lain, setContent
menangkap field
, cellI
dan rowI
sebagai rowI
. cellI
dan rowI
tetap sama, tetapi field
berubah dengan setiap perubahan sel apa pun.
Mari kita refactor kode kita dan tetap sama setContent
.
Untuk menjaga agar referensi ke setContent
sama, kita harus menyingkirkan penutupannya. Kita bisa menghilangkan penutupan rowI
dan rowI
dengan membuat Cell
kita secara eksplisit meneruskan cellI
dan rowI
ke setContent
. Untuk melakukan field
, kita dapat menggunakan fitur setState
rapi - ia menerima panggilan balik .
const [field, setField] = useState(initialField)
Yang membuat App
terlihat seperti ini
<div> {field.map((row, rowI) => ( <div key={rowI}> {row.map((cell, cellI) => ( <Cell key={`row${rowI}cell${cellI}`} content={cell} rowI={rowI} cellI={cellI} setContent={setCell} /> ))} </div> ))} </div>
Sekarang Cell
harus meneruskan cellI
dan rowI
ke setContent
.
const Cell = ({ content, rowI, cellI, setContent }) => { console.log('cell render') return ( <div onClick={() => setContent(rowI, cellI, randomContent())}> {content} </div> ) }
Demo langsung # 4
Mari kita lihat laporan DevTools.

Apa? Kenapa sih katanya "props induk berubah"? Jadi masalahnya adalah bahwa setiap kali bidang kita diperbarui, App
-render ulang. Oleh karena itu komponen turunannya dirender kembali. Ok Apakah stackoverflow mengatakan sesuatu yang berguna tentang React optimasi kinerja? Internet menyarankan untuk menggunakan shouldComponentUpdate
atau kerabat dekatnya: PureComponent
dan memo
.
const Cell = memo(({ content, rowI, cellI, setContent }) => { console.log('cell render') return ( <div onClick={() => setContent(rowI, cellI, randomContent())}> {content} </div> ) })
Demo langsung # 5
Yay Sekarang hanya satu sel yang dirender kembali setelah isinya berubah. Tapi tunggu ... Apakah ada kejutan? Kami mengikuti praktik terbaik dan mendapatkan hasil yang diharapkan.
Tawa jahat seharusnya ada di sini. Karena saya tidak bersama Anda, tolong, coba sekuat mungkin untuk membayangkannya. Silakan dan tambah size
dalam Demo langsung # 5 . Kali ini Anda mungkin harus pergi dengan jumlah yang sedikit lebih besar. Namun, lag masih ada. Kenapa ???
Mari kita lihat laporan DebTools lagi.

Hanya ada satu render Cell
dan itu cukup cepat, tetapi ada juga render App
, yang memakan waktu cukup lama. Masalahnya adalah bahwa dengan setiap App
setiap Cell
harus membandingkan alat peraga baru dengan alat peraga sebelumnya. Bahkan jika ia memutuskan untuk tidak membuat (yang persis seperti kasus kami), perbandingan itu masih membutuhkan waktu. O (1), tetapi O (1) itu terjadi size
* size
kali!
Langkah 2: Pindahkan ke bawah
Apa yang bisa kita lakukan untuk mengatasinya? Jika rendering App
terlalu mahal, kita harus berhenti rendering App
. Tidak mungkin jika tetap meng-hosting keadaan kita di App
menggunakan useState
, karena itulah yang memicu render kembali. Jadi kita harus menurunkan status kita dan membiarkan masing-masing Cell
berlangganan keadaan sendiri.
Mari kita buat kelas khusus yang akan menjadi wadah bagi negara kita.
class Field { constructor(fieldSize) { this.size = fieldSize
Maka App
kita bisa terlihat seperti ini:
const App = () => { return ( <div> {// As you can see we still need to iterate over our state to get indexes. field.map((row, rowI) => ( <div key={rowI}> {row.map((cell, cellI) => ( <Cell key={`row${rowI}cell${cellI}`} rowI={rowI} cellI={cellI} /> ))} </div> ))} </div> ) }
Dan Cell
kami dapat menampilkan konten dari field
sendiri:
const Cell = ({ rowI, cellI }) => { console.log('cell render') const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) }
Demo langsung # 6
Pada titik ini, kita bisa melihat bidang kita dirender. Namun, jika kita mengklik sel, tidak ada yang terjadi. Dalam log kita bisa melihat "setCell" untuk setiap klik, tetapi sel tetap kosong. Alasannya di sini adalah bahwa tidak ada yang memberitahu sel untuk membuat ulang. Status kami di luar Bereaksi berubah, tetapi Bereaksi tidak mengetahuinya. Itu harus berubah.
Bagaimana kita dapat memicu render secara terprogram?
Dengan kelas kami memiliki forceUpdate . Apakah ini berarti kita harus menulis ulang kode kita ke kelas? Tidak juga. Apa yang bisa kita lakukan dengan komponen fungsional adalah untuk memperkenalkan beberapa kondisi dummy, yang kita ubah hanya untuk memaksa komponen kita untuk dirender ulang.
Inilah cara kami dapat membuat kait kustom untuk memaksa rendering ulang.
const useForceRender = () => { const [, setDummy] = useState(0) const forceRender = useCallback(() => setDummy((oldVal) => oldVal + 1), []) return forceRender }
Untuk memicu render ulang saat bidang kami diperbarui, kami harus tahu kapan pembaruan. Itu berarti kita harus dapat berlangganan entah bagaimana untuk pembaruan lapangan.
class Field { constructor(fieldSize) { this.size = fieldSize this.data = new Array(this.size).fill(new Array(this.size).fill(undefined)) this.subscribers = {} } _cellSubscriberId(rowI, cellI) { return `row${rowI}cell${cellI}` } cellContent(rowI, cellI) { return this.data[rowI][cellI] } setCell(rowI, cellI, newContent) { console.log('setCell') this.data = [ ...this.data.slice(0, rowI), [ ...this.data[rowI].slice(0, cellI), newContent, ...this.data[rowI].slice(cellI + 1), ], ...this.data.slice(rowI + 1), ] const cellSubscriber = this.subscribers[this._cellSubscriberId(rowI, cellI)] if (cellSubscriber) { cellSubscriber() } } map(cb) { return this.data.map(cb) }
Sekarang kita bisa berlangganan pembaruan lapangan.
const Cell = ({ rowI, cellI }) => { console.log('cell render') const forceRender = useForceRender() useEffect(() => field.subscribeCellUpdates(rowI, cellI, forceRender), [ forceRender, ]) const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) }
Demo langsung # 7
Mari bermain dengan size
dengan implementasi ini. Coba tingkatkan ke nilai-nilai yang terasa tertinggal sebelumnya. Dan ... Saatnya untuk membuka sebotol sampanye yang bagus! Kami mendapatkan aplikasi yang menjadikan satu sel dan satu sel hanya saat keadaan sel berubah!
Mari kita lihat laporan DevTools.

Seperti yang bisa kita lihat sekarang, hanya Cell
yang dirender dan cepat gila.
Bagaimana jika mengatakan bahwa sekarang kode Cell
kita berpotensi menyebabkan kebocoran memori? Seperti yang Anda lihat, di useEffect
kami berlangganan pembaruan sel, tetapi kami tidak pernah berhenti berlangganan. Ini berarti bahwa bahkan ketika Cell
dihancurkan, langganannya tetap hidup. Mari kita ubah itu.
Pertama, kita perlu mengajari Field
apa artinya berhenti berlangganan.
class Field {
Sekarang kita dapat menerapkan unsubscribeCellUpdates
ke Cell
kita.
const Cell = ({ rowI, cellI }) => { console.log('cell render') const forceRender = useForceRender() useEffect(() => { field.subscribeCellUpdates(rowI, cellI, forceRender) return () => field.unsubscribeCellUpdates(rowI, cellI) }, [forceRender]) const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) }
Demo langsung # 8
Jadi apa pelajarannya di sini? Kapan masuk akal untuk memindahkan status ke bawah pohon komponen? Tidak pernah! Yah, tidak terlalu :) Tetap berpegang pada praktik terbaik sampai gagal dan tidak melakukan optimasi prematur. Jujur, kasus yang kami pertimbangkan di atas agak spesifik, namun, saya harap Anda akan mengingatnya kembali jika Anda perlu menampilkan daftar yang sangat besar.
Langkah bonus: Refactoring dunia nyata
Dalam demo langsung # 8 kami menggunakan field
global, yang seharusnya tidak menjadi kasus di aplikasi dunia nyata. Untuk mengatasinya, kita dapat menginangi field
di App
kita dan meneruskannya menggunakan pohon menggunakan [konteks] ().
const AppContext = createContext() const App = () => {
Sekarang kita dapat mengkonsumsi field
dari konteks di Cell
kita.
const Cell = ({ rowI, cellI }) => { console.log('cell render') const forceRender = useForceRender() const field = useContext(AppContext) useEffect(() => { field.subscribeCellUpdates(rowI, cellI, forceRender) return () => field.unsubscribeCellUpdates(rowI, cellI) }, [forceRender]) const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) }
Demo langsung # 9
Semoga Anda menemukan sesuatu yang berguna untuk proyek Anda. Silakan sampaikan umpan balik Anda kepada saya! Saya sangat menghargai kritik dan pertanyaan apa pun.