Concurrent Mode di React: mengadaptasi aplikasi web untuk perangkat dan kecepatan internet

Pada artikel ini, saya akan memperkenalkan Concurrent Mode di React. Mari kita cari tahu apa itu: apa saja fiturnya, alat baru apa yang telah muncul dan bagaimana mengoptimalkan pengoperasian aplikasi web dengan bantuan mereka sehingga semuanya dapat terbang bagi pengguna. Mode bersamaan adalah fitur baru di React. Tugasnya adalah menyesuaikan aplikasi ke berbagai perangkat dan kecepatan jaringan. Sejauh ini, Concurrent Mode adalah eksperimen yang dapat diubah oleh pengembang pustaka, yang berarti tidak ada alat baru di stabil. Aku memperingatkanmu, dan sekarang ayo pergi.



Saat ini, ada dua batasan untuk merender komponen: daya prosesor dan kecepatan transfer data jaringan. Kapan pun sesuatu perlu ditampilkan kepada pengguna, versi React saat ini mencoba merender setiap komponen dari awal hingga akhir. Tidak masalah jika antarmuka mungkin membeku selama beberapa detik. Ini cerita yang sama dengan transfer data. React akan benar-benar menunggu semua data yang dibutuhkan komponen, daripada menggambarnya sepotong demi sepotong.







Rezim kompetitif memecahkan masalah ini. Dengannya, React dapat menjeda, memprioritaskan, dan bahkan membatalkan operasi yang sebelumnya memblokir, jadi dalam mode bersamaan Anda dapat mulai merender komponen terlepas dari apakah semua data telah diterima atau hanya sebagian saja.



Concurrent Mode adalah Fiber Architecture



Mode kompetitif bukanlah hal baru yang tiba-tiba diputuskan oleh pengembang untuk ditambahkan, dan semuanya berfungsi di sana. Dipersiapkan untuk peluncurannya terlebih dahulu. Di versi 16, mesin React telah dialihkan ke arsitektur Fiber, yang pada prinsipnya menyerupai penjadwal tugas di sistem operasi. Penjadwal mendistribusikan sumber daya komputasi antar proses. Dia dapat beralih kapan saja, sehingga pengguna memiliki ilusi bahwa proses tersebut berjalan secara paralel.



Arsitektur fiber melakukan hal yang sama, tetapi dengan komponen. Terlepas dari kenyataan bahwa itu sudah ada di React, arsitektur Fiber tampaknya dalam keadaan mati suri dan tidak menggunakan kemampuannya secara maksimal. Mode Kompetitif akan mengaktifkannya dengan kekuatan penuh.



Saat memperbarui komponen dalam mode normal, Anda harus menggambar bingkai baru di layar. Hingga pembaruan selesai, pengguna tidak akan melihat apa pun. Dalam hal ini, React bekerja secara serempak. Fiber menggunakan konsep yang berbeda. Setiap 16 ms ada interupsi dan pemeriksaan: apakah pohon virtual berubah, apakah data baru muncul? Jika demikian, pengguna akan segera melihatnya.



Mengapa 16ms? Pengembang React berusaha untuk menggambar ulang layar dengan kecepatan mendekati 60 frame per detik. Untuk menyesuaikan 60 pembaruan menjadi 1000 md, Anda perlu melakukannya kira-kira setiap 16 md. Karena itu sosoknya. Mode Kompetitif keluar dari kotak dan menambahkan alat baru yang membuat kehidupan front-end lebih baik. Saya akan memberi tahu Anda tentang masing-masing secara rinci.



Ketegangan



Suspensi diperkenalkan di React 16.6 sebagai mekanisme untuk memuat komponen secara dinamis. Dalam mode bersamaan, logika ini dipertahankan, tetapi peluang tambahan muncul. Suspense menjadi mekanisme yang bekerja terkait dengan pustaka pemuatan data. Kami meminta sumber daya khusus melalui perpustakaan dan membaca data darinya.



Suspense membaca data yang belum siap secara bersamaan. Bagaimana? Kami meminta datanya, dan sampai lengkap, kami sudah mulai membacanya dalam potongan-potongan kecil. Hal paling keren bagi pengembang adalah mengelola urutan tampilan data yang dimuat. Suspense memungkinkan Anda menampilkan komponen halaman secara bersamaan dan terpisah satu sama lain. Itu membuat kode mudah: Anda hanya perlu melihat struktur Suspense untuk melihat urutan data yang diminta.



Solusi umum untuk memuat halaman di React "lama" adalah Fetch-On-Render. Dalam kasus ini, kami meminta data setelah render di dalam useEffect atau componentDidMount. Ini adalah logika standar ketika tidak ada Redux atau lapisan data lainnya. Misalnya, kami ingin menggambar 2 komponen, yang masing-masing membutuhkan data:



  • Permintaan komponen 1
  • Harapan…
  • Dapatkan data -> render komponen 1
  • Permintaan komponen 2
  • Harapan…
  • Dapatkan data -> render komponen 2


Dalam pendekatan ini, komponen berikutnya diminta hanya setelah yang pertama dirender. Itu panjang dan tidak nyaman.



Mari pertimbangkan cara lain, Fetch-Then-Render: pertama kita meminta semua data, lalu kita menggambar halamannya.



  • Permintaan komponen 1
  • Permintaan komponen 2
  • Harapan…
  • Mendapatkan komponen 1
  • Mendapatkan komponen 2
  • Rendering komponen


Dalam kasus ini, kami memindahkan status permintaan ke suatu tempat ke atas - kami mendelegasikannya ke pustaka untuk bekerja dengan data. Metodenya bekerja dengan baik, tetapi ada nuansa. Jika salah satu komponen membutuhkan waktu lebih lama untuk dimuat daripada yang lain, pengguna tidak akan melihat apa pun, meskipun kami sudah menunjukkan sesuatu kepadanya. Mari kita lihat contoh kode dari demo dengan 2 komponen: Pengguna dan Posting. Kami membungkus komponen dalam Suspense:



const resource = fetchData() // -    React
function Page({ resource }) {
    return (
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User resource={resource} />
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={resource} />
            </Suspense>
        </Suspense>
    )
}


Tampaknya pendekatan ini mirip dengan Fetch-On-Render, saat kami meminta data setelah merender komponen pertama. Namun nyatanya, menggunakan Suspense akan mendapatkan data lebih cepat. Ini karena fakta bahwa kedua permintaan dikirim secara paralel.



Dalam Suspense, Anda dapat menentukan fallback, komponen yang ingin kita tampilkan, dan meneruskan sumber daya yang diimplementasikan oleh pustaka pengambilan data di dalam komponen. Kami menggunakannya sebagaimana adanya. Di dalam komponen, kami meminta data dari sumber daya dan memanggil metode baca. Ini adalah janji yang dibuat perpustakaan untuk kita. Suspense akan memahami jika data telah dimuat, dan jika demikian, akan menampilkannya.



Perhatikan bahwa komponen mencoba membaca data yang masih dalam proses diterima:



function User() {
    const user = resource.user.read()
    return <h1>{user.name}</h1>
}
function Posts() {
    const posts = resource.posts.read()
    return //  
}


Dalam demo Dan Abramov saat ini, hal seperti itu digunakan sebagai rintisan untuk sumber daya .



read() {
    if (status === 'pending') {
        throw suspender
    } else if (status === 'error') {
        throw result
    } else if (status === 'success') {
        return result
    }
}




Jika sumber daya masih dimuat, kami menampilkan objek Promise sebagai pengecualian. Suspense menangkap pengecualian ini, menyadarinya sebagai Janji, dan terus memuat. Jika, alih-alih Promise, pengecualian dengan objek lain datang, akan menjadi jelas bahwa permintaan berakhir dengan kesalahan. Ketika hasil akhirnya dikembalikan, Suspense akan menampilkannya. Penting bagi kita untuk mendapatkan sumber daya dan memanggil metode di atasnya. Bagaimana itu diterapkan secara internal adalah keputusan pengembang perpustakaan, yang utama adalah Suspense memahami implementasinya.



Kapan meminta data? Menanyakan di atas pohon bukanlah ide yang baik, karena mungkin tidak pernah diminta. Pilihan yang lebih baik adalah melakukan ini segera saat menavigasi di dalam penangan acara. Misalnya, dapatkan status awal melalui hook, lalu buat permintaan resource segera setelah pengguna mengklik tombol.



Beginilah tampilannya dalam kode:



function App() {
    const [resource, setResource] = useState(initialResource)
    return (
        <>
            <Button text='' onClick={() => {
                setResource(fetchData())
            }}>
            <Page resource={resource} />
        </>
    );
}


Ketegangan sangatlah fleksibel. Ini dapat digunakan untuk menampilkan komponen satu demi satu.



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User />
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
    </Suspense>
)


Atau pada saat yang bersamaan, maka kedua komponen tersebut perlu dibungkus dalam satu Suspense.



return (
    <Suspense fallback={<h1>Loading user and posts...</h1>}>
        <User />
        <Posts />
    </Suspense>
)


Atau, muat komponen secara terpisah satu sama lain dengan membungkusnya dalam Suspensi independen. Sumber daya akan dimuat melalui perpustakaan. Ini sangat keren dan nyaman.



return (
    <>
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User />
        </Suspense>
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
    </>
)


Selain itu, komponen Error Boundary akan mendeteksi error di dalam Suspense. Jika ada yang tidak beres, kami dapat menunjukkan bahwa pengguna telah memuat, tetapi posting belum, dan memberikan kesalahan.



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User resource={resource} />
        <ErrorBoundary fallback={<h2>Could not fetch posts</h2>}>
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={resource} />
            </Suspense>
        </ErrorBoundary>
    </Suspense>
)


Sekarang mari kita lihat alat lain yang dapat sepenuhnya memberikan manfaat penuh dari rezim persaingan.



SuspenseList



SuspenseList secara bersamaan membantu mengontrol urutan pemuatan Suspense. Jika kami perlu memuat beberapa Suspense secara ketat satu demi satu tanpa itu, mereka harus bertumpuk di dalam satu sama lain:



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User />
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
            <Suspense fallback={<h1>Loading facts...</h1>}>
                <Facts />
            </Suspense>
        </Suspense>
    </Suspense>
)


SuspenseList membuatnya lebih mudah:



return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
        <Suspense fallback={<h1>Loading facts...</h1>}>
            <Facts />
        </Suspense>
    </Suspense>
)


Fleksibilitas SuspenseList luar biasa. Anda dapat menyarangkan SuspenseList sesuka Anda dan menyesuaikan urutan pemuatan di dalamnya karena akan memudahkan untuk menampilkan widget dan komponen lainnya.



useTransition



Hook khusus yang menunda pembaruan komponen hingga benar-benar siap dan menghapus status pemuatan perantara. Untuk apa ini? React berusaha untuk melakukan transisi secepat mungkin saat mengubah status. Tapi terkadang penting untuk meluangkan waktu Anda. Jika sebagian data dimuat atas tindakan pengguna, maka biasanya pada saat memuat kami menampilkan loader atau kerangka. Jika data tiba dengan sangat cepat, maka loader tidak akan punya waktu untuk menyelesaikannya bahkan setengah putaran. Ini akan berkedip, lalu menghilang, dan kami akan menggambar komponen yang diperbarui. Dalam kasus seperti itu, lebih bijaksana untuk tidak menampilkan loader sama sekali.



Di sinilah useTransition masuk. Bagaimana cara kerjanya dalam kode? Kami memanggil hook useTransition dan menentukan batas waktu dalam milidetik. Jika data tidak datang dalam waktu yang ditentukan, maka kami akan tetap menampilkan loader. Tetapi jika kita mendapatkannya lebih cepat, akan ada transisi instan.



function App() {
    const [resource, setResource] = useState(initialResource)
    const [startTransition, isPending] = useTransition({ timeoutMs: 2000 })
    return <>
        <Button text='' disabled={isPending} onClick={() => {
            startTransition(() => {
                setResource(fetchData())
            })
        }}>
        <Page resource={resource} />
    </>
}


Terkadang kami tidak ingin menampilkan loader saat membuka halaman, tetapi kami masih perlu mengubah sesuatu di antarmuka. Misalnya, selama transisi, blokir tombol. Kemudian properti isPending akan berguna - ini akan memberi tahu Anda bahwa kami sedang dalam tahap transisi. Untuk pengguna, pembaruan akan instan, tetapi penting untuk diperhatikan di sini bahwa sihir useTransition hanya memengaruhi komponen yang dibungkus dalam Suspense. UseTransition itu sendiri tidak akan berfungsi.



Transisi biasa terjadi pada antarmuka. Logika yang bertanggung jawab untuk transisi akan sangat bagus untuk dijahit ke dalam tombol dan diintegrasikan ke dalam perpustakaan. Jika ada komponen yang bertanggung jawab untuk transisi antar halaman, Anda dapat menggabungkan onClick yang diteruskan melalui props ke tombol di handleClick dan menampilkan status isDisabled.



function Button({ text, onClick }) {
    const [startTransition, isPending] = useTransition({ timeoutMs: 2000 })

    function handleClick() {
        startTransition(() => {
            onClick()
        })
    }

    return <button onClick={handleClick} disabled={isPending}>text</button>
}


useDeferredValue



Jadi, ada komponen yang kami gunakan untuk melakukan transisi. Terkadang situasi berikut muncul: pengguna ingin pergi ke halaman lain, kami telah menerima beberapa data dan siap untuk menampilkannya. Pada saat yang sama, halaman-halaman tersebut sedikit berbeda satu sama lain. Dalam kasus ini, akan logis untuk menampilkan data lama pengguna sampai yang lainnya dimuat.



Sekarang React tidak tahu caranya: dalam versi saat ini, hanya data dari kondisi saat ini yang dapat ditampilkan di layar pengguna. Namun useDeferredValue dalam mode serentak dapat mengembalikan versi nilai yang ditangguhkan, menampilkan data lama, bukan loader yang berkedip atau fallback saat boot. Hook ini mengambil nilai yang kita inginkan untuk mendapatkan versi yang ditangguhkan dan penundaan dalam milidetik.



Antarmuka menjadi sangat lancar. Pembaruan dapat dilakukan dengan jumlah data minimum, dan yang lainnya dimuat secara bertahap. Pengguna mendapat kesan bahwa aplikasinya cepat dan lancar. Dalam aksinya, useDeferredValue terlihat seperti ini:



function Page({ resource }) {
    const deferredResource = useDeferredValue(resource, { timeoutMs: 1000 })
    const isDeferred = resource !== deferredResource;
    return (
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User resource={resource} />
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={deferredResource} isDeferred={isDeferred}/>
            </Suspense>
        </Suspense>
    )
}


Anda dapat membandingkan nilai dari props dengan yang diperoleh melalui useDeferredValue. Jika berbeda, halaman masih memuat.



Menariknya, useDeferredValue memungkinkan Anda mengulangi trik pemuatan lambat, tidak hanya untuk data yang dikirim melalui jaringan, tetapi juga untuk menghapus penghentian antarmuka karena penghitungan yang besar.



Mengapa ini bagus? Perangkat yang berbeda bekerja secara berbeda. Jika Anda menjalankan aplikasi menggunakan useDeferredValue di iPhone baru, transisi dari halaman ke halaman akan terjadi secara instan, meskipun halamannya berat. Tetapi saat menggunakan debounce, penundaan akan muncul bahkan pada perangkat yang kuat. UseDeferredValue dan mode serentak beradaptasi dengan perangkat keras: jika berfungsi lambat, masukan akan tetap muncul, dan laman itu sendiri akan diperbarui jika perangkat mengizinkan.



Bagaimana cara mengalihkan proyek ke Mode Bersamaan?



Mode Kompetitif adalah mode, jadi Anda harus mengaktifkannya. Seperti sakelar sakelar yang membuat Fiber bekerja dengan kapasitas penuh. Mulai dari mana



Kami menghapus warisan. Kami menyingkirkan semua metode usang dalam kode dan memastikan bahwa metode tersebut tidak ada di perpustakaan. Jika aplikasi berfungsi dengan baik di React.StrictMode, maka semuanya baik-baik saja - pemindahan akan mudah. Komplikasi potensial adalah masalah dalam perpustakaan. Dalam kasus ini, Anda perlu memutakhirkan ke versi baru, atau mengubah pustaka. Atau tinggalkan rezim kompetitif. Setelah menghilangkan legacy, yang tersisa hanyalah mengganti root.



Dengan kedatangan Concurrent Mode, tiga mode koneksi root akan tersedia:



  • Mode

    ReactDOM.render(<App />, rootNode)

    Render Lama akan dihentikan setelah mode kompetitif dirilis.
  • Mode pemblokiran

    ReactDOM.createBlockingRoot(rootNode).render(<App />)

    Sebagai tahap perantara, mode pemblokiran akan ditambahkan, yang memberikan akses ke beberapa peluang untuk mode kompetitif pada proyek di mana terdapat warisan atau kesulitan lain dengan relokasi.
  • Mode kompetitif

    ReactDOM.createRoot(rootNode).render(<App />)

    Jika semuanya baik-baik saja, tidak ada warisan, dan proyek dapat segera dialihkan, ganti render dalam proyek dengan createRoot - dan nonaktifkan ke masa depan yang cerah.


kesimpulan



Operasi pemblokiran di dalam React dibuat tidak sinkron dengan beralih ke Fiber. Alat-alat baru bermunculan yang memudahkan untuk menyesuaikan aplikasi dengan kemampuan perangkat dan kecepatan jaringan:



  • Ketegangan, berkat itu Anda dapat menentukan urutan memuat data.
  • SuspenseList, yang bahkan lebih nyaman.
  • useTransition untuk membuat transisi yang mulus antara komponen yang dibungkus Suspense.
  • useDeferredValue - untuk menampilkan data usang selama I / O dan pembaruan komponen


Cobalah bereksperimen dengan Concurrent Mode saat masih tersedia. Concurrent Mode memungkinkan Anda mencapai hasil yang mengesankan: pemuatan komponen yang cepat dan lancar dalam urutan yang nyaman, antarmuka super lancar. Detailnya dijelaskan dalam dokumentasi, ada demo dengan contoh yang perlu Anda telusuri sendiri. Dan jika Anda penasaran tentang cara kerja arsitektur Fiber, berikut tautan ke pembicaraan yang menarik.



Evaluasi proyek Anda - apa yang dapat ditingkatkan dengan alat baru? Dan ketika rezim kompetitif keluar, silakan bergerak. Semuanya akan bagus!



All Articles