Uploaded by Muhammad kajuke

Optimasi Kinerja React: Profiler, Memo, dan Hooks

advertisement
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
Download