REACT OPTIMIZATION #optimization Performance Optimization pada react dapat dilakukan dengan memperhatikan: - Prevent Wasted Renders - Improve App Speed/Responsiveness - Reduce Bundle Size Performance Optimization and their tools: ![[Pasted image 20231016104558.png]] --## Profiler Tools Profiler tools merupakan tools pada react developer tools untuk melihat performance dari sebuah react app ![[Pasted image 20231016105735.png]] Tools ini sangat berguna dalam performance optimization karena dapat melihat component apa aja yang dirender, kenapa dirender, dan berapa lama di rendernya. ### Cara Menggunakannya - Centang dulu biar kita dapat melihat kenapa component di render. Jangan di refresh karena harus centang lagi ![[Pasted image 20231016105907.png]] - Start Profiling ![[Screenshot 2023-10-16 110008.png]] - Gunakan apps sebagaimana mestinya (supaya mentrigger render) - Klik Stop Profiling (Tombol Merah) ![[Pasted image 20231016110253.png]] #### Penjelasan ![[Pasted image 20231016110347.png]] Pada hasilnya kita dapat melihat flamegraph chart yang merupakan semua component yang berada pada screen. **Grey** artinya dia ga re-render dan warna lainnya menunjukkan bahwa component re-render. Kita juga dapat melihat mengapa component tersebut re-render dengan hover kepada component yang terdapat pada graphnya ![[Pasted image 20231016110637.png]] Pada bagian bar juga kita dapat melihat berapa kali state update dan membutuhkan berapa lama untuk re-render ![[Pasted image 20231016110717.png]] Kita juga dapat melihat ranked chart yang merupakan kecepatan re-render dari setiap component pada layar ![[Pasted image 20231016110852.png]] --## Wasted Render **Wasted Render** merupakan sebuah proses render yang tidak menghasilkan perubahan pada DOM. Untuk memahaminya kita perlu tau bagaimana sebuah component instance mengalami re-render. Component akan mengalami re-render ketika: - State Changes - Context Changes - Parent re-renders Miskonsepesi umum adalah component instances akan re-render ketika props berubah. Props akan berubah ketika parent nya berubah maka terjadi re-render. Hal ini menunjukkan bahwa ketika parent component re-renders maka children nya akan re -renders juga. Re-render **bukan berarti sebuah DOM mengalami perubahan/update**. Akan tetapi berarti sebuah function components dipanggil lagi. Hal ini akan menyebabkan operation yang expensive. Yang menjadi masalah adalah re-render happen too frequently or when the component is very slow (laggy) ![[Pasted image 20231016104636.png]] --- ## Optimization Trick With React Children Salah satu optimization trick yang paling **suprising** adalah dengan menggunakan children. Misalkan kita mau mengenerate 100.000 list of word di dalam sebuah component yang mana component tersebut ada counter. ```jsx function SlowComponent() { // If this is too slow on your maching, reduce the `length` const words = Array.from({ length: 100_000 }, () => "WORD"); return ( <ul> {words.map((word, i) => ( <li key={i}> {i}: {word} </li> ))} </ul> ); } export default function Test() { const [count, setCount] = useState(0); return ( <div> <h1>Slow counter?!?</h1> <button onClick={() => setCount((c) => c + 1)}>Increase: {count}</button> {/*generate 100.000 list of word*/} <SlowComponent /> </div> ); } ``` Ketika click add counternya maka akan terjadi delay yang sangat sangat besar (lambar). Hal ini terjadi karena setiap kita click increase maka child component (dalam hal ini `<SlowComponent/>`) akan ikut rerender. Setiap 1 klik maka akan re-render 100.000 list of word terus terusan. ![[Pasted image 20231016114339.png]] Untuk mengatasi hal tersebut maka kita mau supaya setiap klik component tersebut tidak re-render. Hal tersebut dapat dilakukan dengan react children. ```jsx function Optimization({ children }) { const [count, setCount] = useState(0); return ( <div> <h1>Slow counter?!?</h1> <button onClick={() => setCount((c) => c + 1)}>Increase: {count}</button> {/*OPTIMIZATION USING CHILDREN*/} {children} </div> ); } export default function Test() { return ( <div> {/*OPTIMIZATION WITH CHILDREN*/} <Optimization> <SlowComponent /> </Optimization> </div> ); } ``` Buat sebuah wraper component yang menerima se buah children sehingga delay yang besar tersebut dapat teratasi. **MENGAPA HAL TERSEBUT BISA DI OPTIMIZE????** Hal tersebut bisa di optimize karena react akan membaca codenya dan mengetahui bahwa `<SlowComponent/>` telah dibuat (ter render) terlebih dahulu, jadi apapun yang terjadi pada perubahan state di counter tidak akan berpengaruh terhadap childrennya. ![[Pasted image 20231016115412.png]] Hal ini sebenarnya sudah terimplementasi pada Context karena component - component yang tidak menggunakan suatu context atau menggunakan context tapi bukan merupakan value yang berubah/updated, tidak akan mengalami re-render. --- ## Memo Memoization merupakan teknik optimization yang executes a pure function once and save the result in memory. Jika kita mencoba untuk execute function lagi dengan argumen yang sama dengan sebelumnya, maka saved result akan di return tanpa executing functionnya lagi. Akan tetapi apabila kita execute function dengan argumen baru maka resultnya akan baru lagi (diexecute lagi). ![[Pasted image 20231016135050.png]] Memoize dapat terbagi menjadi - Memoize **components** dengan `memo` - Memoize **object** dengan `useMemo` - Memoize **function** dengan `useCallback` Memoization bertujuan untuk - Prevent wasted renders - Improve app speed/responsiveness --- ### Memo Function Memo pada react berfungsi untuk membuat component yang tidak akan mengalami re-render ketika parent re-renders, **as long as propsnya sama setiap renders** **Memo pada react hanya berpengaruh oleh props**. Component yang ter memoized akan mengalami re-render ketika statenya berubah atau context yang dipakainya berubah Memo hanya dipakai ketika component lambat (slow rendering), re-renders often, dan mempunyai props yang sama. Jadi semua component tidak dapat di memoized. ![[Pasted image 20231016135928.png]] --### Memo in Practice Misal kita mempunyai component `<Archive/>` yang berfungsi untuk menampilkan 30.000 archive post. Component ini terdapat pada App component dan dapat di trigger dengan klik button show archive. ![[Pasted image 20231016143626.png]] ![[Pasted image 20231016143752.png]] Perhatikan saat sebelum dan sesudah show archive, jika kita menuliskan pada search bar maka akan terjadi lag karena perubahan state component pada app mentrigger semua component untuk re-renders sehingga 30.000 post kembali di re-renders yang menyebabkan aplikasi menjadi berat. ![[Pasted image 20231016144123.png]] - Bar 1 menunjukkan latency ketika kita mengetik pada searchbar sebelum show archive (sebelum 30.000 data di load) - Bar 2 menunjukkan ketika kita merender archive - Bar 3 menunjukkan latency ketika kita mengetik pada searchbar sesudah show archive (sesudah 30.000 data di load) Hal tersebut dapat diatasi dengan menggunakan `memo` function dari react ```jsx import {memo} from "react" const Archive = memo(function Archive({ show }) { ... }); ``` ![[Pasted image 20231016144611.png]] pada bar 3 memo function berhasil mengoptimize dengan tidak merender archive karena propsnya masih sama dan tidak berubah (yaitu *show*) kecuali saat di trigger untuk show 30.000 archive postnya (spike pada bar 2). #### Cara Lain Untuk Memo Component ```jsx function Navbar(){ ... } //di memoize saat di export export default memo(Navbar) ``` --### Understanding useMemo and useCallback #### Problem dengan Memo - Pada react semua **hal akan di buat ulang (re-created) pada setiap render** (termasuk objek dan functions) - Pada Javascript **Object atau Function** bisa saja terlihat sama dengan code yang sama, akan tetapi keduanya **merupakan hal yang berbeda** sebagai contoh sebuah empty objek tidak sama dengan empty objek ({ } != { }) - Apabila **object atau functions di passed sebagai props**, maka **child componentnya akan melihatnya sebagai props baru pada setiap re-render** - Jika props berbeda pada setiap re-render maka memo tidak akan berfungsi - Kita perlu cara untuk me memoize object atau function untuk membuatnya stable (preserve) between re-renders (memoized { } == memoize{ }) ![[Pasted image 20231016161817.png]] #### useMemo dan useCallback `useMemo` dan `useCallback` merupakan hooks yang digunakan untuk memoize values (dengan `useMemo`) dan functions (dengan `useCallback`) pada setiap renders, sehingga sebuah values atau sebuah function preserve on every render Values yang di passed in ke `useMemo` dan `useCallback` akan disimpan di dalam memory ("cached") dan akan di return pada setiap re-renders asalkan dependencies ("input") tetap sama `useMemo` dan `useCallback` mempunyai dependency array (seperti `useEffect`), apabila satu depedency berubah, maka value yang baru akan re-created ![[Pasted image 20231016162311.png]] Bukan berarti semua object dan function yang terdapat pada aplikasi harus di memoize, memoize hanya dilakukan dalam 3 use cases yaitu: ![[Pasted image 20231016162449.png]] #### useMemo in Practice Misalkan kita ingin me memoize sebuah object yang di pass in sebagai props. Hal ini karena objectnya akan terus di re-created sama react sehingga memo tidak akan berfungsi pada props yang berbeda pada setiap renders-nya (walaupun objectnya sama tetep aja berbeda) ```jsx import {useMemo} from "react" const archiveOptions = useMemo(() => { return { show: false, title: "Post Archive in Addition to main Posts", } }, []); ``` `useMemo` hooks menerima 2 argumen yaitu function yang akan dipanggil pada initial renders dan dependecy arrays. Pada code tersebut function akan mereturn object yang mau kita memoize, ketika function dipanggil maka object tersebut akan disimpan di memory ("cached"). **Bagaimana dengan object yang akan berubah valuesnya???** ```jsx import {useMemo} from "react" ... const [post, setPost] = useState([]) const archiveOptions = useMemo(() => { return { show: false, title: `Post Archive ${post.length} in Addition to main Posts`, } }, [post.length]); ``` dependency arrays harus di specify valuesnya yang akan berubah. Jadi setiap `post.length` berubah maka objectnya juga akan di re-created ulang. #### useCallback `useCallback` digunakan pada sebuah function yang dapat memberatkan component. Sebagai contoh ![[Pasted image 20231016170517.png]] onAddPost akan dilihat react sebagai props yang baru karena tidak sama dengan props sebelumnya (padahal sama). Maka kita dapat me memoize function tersebut ```jsx import {useCallback} from 'react' const handleAddPost = useCallback(function handleAddPost(post) { setPosts((posts) => [post, ...posts]); }, []); } ``` `useCallback` menerima 2 argumen yaitu function yang kita ingin memoize, dan dependecy array. Berbeda dengan `useMemo` yang simpan hasil return pada sebuah callback, yang di memoize atau di simpan dalam `useCallback` adalah functionnya. **Kenapa setPosts tidak dimasukkan ke dalam dependecy arrays?**. Hal tersebut karena setiap `set` function dari react (dalam hal ini `useState`) sudah di memoize dan valuenya akan sama (persisted) jadi tidak di re-create on every render. Hasil sesudah function di memoize: ![[Pasted image 20231016171029.png]] #### useEffect infinite loop optimization Ketika kita dihadapkan oleh infinite loop pada sebuah function useEffect kita dapat melakukan optimization dengan useCallback. ```jsx useEffect( function () { getCity(id); }, {/*Infinite Loop memanggil API akan terjadi akibat `getCity` pada dependecy array */} [id, getCity] ); ``` Mengapa infinite loop terjadi?, ketika kita menspecify sebuah function pada dependecy array seperti contoh `[id, getCity]`, setiap ada perubahan dan render pada getCity maka function akan terus dipanggil pada setiap re-renders, hal tersebut terjadi karena function pada setiap render di re-created dan menjadi baru sehingga terus menerus dipanggil. Maka solusinya adalah dengan memoize functionnya menggunakan `useCallback` ```jsx const getCity = memo(async function getCity(id) { if (currentCity.id === Number(id)) return; dispatch({ type: "loading" }); try { const res = await fetch(`${BASE_URL}/cities/${id}`); const data = await res.json(); dispatch({ type: "city/loaded", payload: data }); } catch { dispatch({ type: "rejected", payload: "There is an error while fetching the data...", }); } }); ``` --### Context Optimization Optimization pada context harus memperhatikan 3 hal yaitu: - State pada context berubah setiap saat - Banyak consumer contextnya - Aplikasi terasa sangat berat (laggy) Optimization dapat dilakukan ketika terdapat perubahan contoh pada state App (diatas provider) ```jsx <App> <Context.Provider> ... </Context.provider> </App> ``` Saat terjadi perubahan state pada App maka akan mentrigger semua child component re-render. Diantaranya maka Context Providernya akan re-render juga menyebabkan component - component yang consume contextnya akan re-render karena objext dari value akan terus menerus di re-created. ![[Pasted image 20231016233632.png]] Component List re-render karena context berubah padahal yang berubah hanyalah state di App. Hal ini mencirikan terdapat waste render sehingga diperlukan optimization pada object value di contextnya ```jsx const value = useMemo(() => { return { posts: searchedPosts, onClearPosts: handleClearPosts, onAddPost: handleAddPost, searchQuery, setSearchQuery, }; }, [searchQuery, searchedPosts]); return <PostContext.Provider value={value}>{children}</PostContext.Provider>; } ``` --## Optimizing Bundling Size with Code Splitting Ketika user melakukan request pada sebuah website, server akan mengirimkan **bundle** ![[Pasted image 20231018173945.png]] - `Bundle`: merupakan javascript file yang terdiri dari semua code aplikasi. Ketika kita mendownload bundle maka seluruh bundle akan terdownload dan aplikasi web akan berubah menjadi Single Page Application (SPA), jadi web tidak perlu untuk menload lagi datanya pada setiap request. Bundle dibuat oleh Webpack atau Vite (Bundler) - `Bundle Size`: merupakan besaran dari file javascript yang perlu di download oleh user untuk menggunakan aplikasi. `Bundle Size` perlu dioptimisasi sehingga file yang perlu didownload menjadi kecil dan memerlukan waktu yang sebentar. Optimisasi dari `bundle size` dapat dilakukan dengan melakukan `Code Splitting`. `Code splitting` merupakan cara optimisasi `bundle size` dengan mensplit bundle menjadi beberapa part yang dapat didownload ketika user merequest bundlenya (lazy loading). ![[Pasted image 20231018182904.png]] Contoh kita ingin mengoptimalkan size pada sebuah aplikasi. Saat ini bundle sizenya memiliki size yang besar. Untuk mengecek bundle size dapat menggunakan `npm run build` ![[Pasted image 20231018183421.png]] React menyarankan kita untuk melakukan code splitting Cara mengoptimalkan size bundle (code splitting): Menggunakan React lazy function. Lazy function digunakan pada **import pages** (**BUKAN COMPONENT BIASA** ). Tentukan import mana saja yang merupakan import pages (biasanya terdapat pada app component di router) ```jsx <BrowserRouter> <Routes> <Route index element={<Homepage />} /> <Route path="pricing" element={<Pricing />} /> <Route path="product" element={<Product />} /> <Route path="login" element={<Login />} /> <Route path="app" element={ <ProtectedRoutes> <AppLayout /> </ProtectedRoutes> } > <Route index replace element={<Navigate to="cities" />} /> <Route path="cities" element={<CityList />} /> <Route path="cities/:id" element={<City />} /> <Route path="countries" element={<CountryList />} /> <Route path="form" element={<Form />} /> </Route> <Route path="*" element={<PageNotFound />} /> </Routes> </BrowserRouter> ``` pada code tersebut kita dapat mengetahui bahwa pages yang utama adalah `Homepage, Pricing, Product, Login, AppLayout, dan PageNotFound`, maka yang dapat kita lakukan adalah menggunakan lazy function pada pages tersebut untuk mengimplementasi lazy loading. ```jsx import {lazy} from 'react' const Homepage = lazy(() => import("./pages/Homepage")) const Pricing = lazy(() => import ("./pages/Pricing")) const Product = lazy(() => import ("./pages/Product")) const PageNotFound = lazy(() => import ("./pages/PageNotFound")) const Login = lazy(() => import ("./pages/Login")) const AppLayout = lazy(() => import ("./pages/AppLayout")) ``` Jika sudah melakukan lazy import kemudian kita menggunakan `<Suspense/>` component dan `fallback` propsnya sehingga setiap kali kita request untuk download chunks of bundlenya (bagian - bagian bundle yang telah kita bagi) terdapat loading pada setiap transisi pagesnya (siapkan component loading pagesnya) ```jsx ... <BrowserRouter> {/* SUSPENSE DAN SPINNERNYA */} <Suspense fallback={<SpinnerFullPage />}> <Routes> <Route index element={<Homepage />} /> <Route path="pricing" element={<Pricing />} /> <Route path="product" element={<Product />} /> <Route path="login" element={<Login />} /> <Route path="app" element={ <ProtectedRoutes> <AppLayout /> </ProtectedRoutes> } > <Route index replace element={<Navigate to="cities" />} /> <Route path="cities" element={<CityList />} /> <Route path="cities/:id" element={<City />} /> <Route path="countries" element={<CountryList />} /> <Route path="form" element={<Form />} /> </Route> <Route path="*" element={<PageNotFound />} /> </Routes> </Suspense> </BrowserRouter> ``` Sehingga dihasilkan terdapat transisi pada setiap kita request chunk of bundle sizenya, dan hasilnya pun terlihat bundle sizenya di split jadi chunk berdasarkan pages yang kita sudah lakukan lazy loading (code splitting). Kalo misalkan ditambah semuanya hasilnya akan tetap size awal bundle (500 an kb) ![[Pasted image 20231018190032.png]] --## Do and Dont's Optimizing DO'S: - Find performance bottlenecks using profiler tools and visual Inspection - Fix those real performances issues - Memoize expensive re-renders - Memoise expensive calculations - Optimize context if it has many consumers and changes often - Memoize Context value + child components - ALWAYS implement code splitting + lazy loading for SPA routes DON"T: - Don't optimize prematurely - Don't optimize anything if there is nothing to optimize - Don't wrap all components in `memo()` - Don't wrap all values in `useMemo()` - Don't wrap all functions in `useCallback()` - Don't optimize context if it's not slow and doesn't have many consumers