--- title: React Reconciliation tags: React description: View the slide with "Slide Mode". --- # Reconciliation (React 效能調整) --- ```javascript= 在這篇文章中描述了實作 React 底層中「diffing」演算法時, 我們採取什麼策略讓 component 的更新是可預測的, 同時可以滿足要求高效能的應用程式。 ``` --- > 當 React 元件遇到效能瓶頸時,可以在重構或解析元件需從 Element、Reconciliation 概念開始了解 > [name=cckai] --- ### Element X JSX 元件中的 JSX 會透過 createElement 這個 function 被轉換成 React Element。 React Element 是一個物件,包含 type 及 properties,type 用來區分是 component instance 或是 DOM node --- Example: ```javascript= <div className="APP"> <TodoItem> Watch a Movie </TodoItem> </div> ``` => 會編譯成 (React Element) ```javascript= { tag: 'div', props: { className: 'APP' } children: [ { tag: 'TodoItem', children: 'Watch a Movie' } ] } ``` --- ### Virtual Dom Virtual DOM 實際上就是用 JavaScript 物件來描述 DOM 結構。 當 DOM 的節點需要更動時,不會直接修改 DOM, 而是**透過 DOM diff 演算法比較 Virtual DOM 修改前與修改後的樹狀結構,再批次更新真實的 DOM 節點**。 `將複雜度從O(n三次方) 調整到 O(n)` :::success 當component更改state的時候就會觸發 render事件 此時react就會產生新的 Virtual DOM 並且拿這個tree 和舊的 tree 用 diff方法去比對 O(n) 找出相異的節點再去作轉換 ::: --- ### Diffing 演算法 當比對兩顆 tree 時,React 首先比較兩棵 tree 的 root element。不同類型的 root element 會有不同的處理方式。 1. 比對不同類型的 Element : 當比對的兩個 root element 為不同類型的元素時,React 會將原有的 tree 整顆拆掉並且重新建立起新的 tree。 例如,當一個元素從 a tag 變成 img、從 Article tag變成 Comment tag、或從 Button 變成 div tag 時,都會觸發一個完整的重建流程。 2. 比對同一類型的 DOM Element 當比對兩個相同類型的 React element 時,React 會保留 DOM 節點,只比對及更新有改變的 attribute。 例如: ```javascript= <div className="before" title="stuff" /> <div className="after" title="stuff" /> ``` 透過比對這兩個 element,React 知道只需要修改 DOM 節點上的 className。 --- ### Reconciliation:React 的渲染機制 在 React 當中,透過 Reconciliation 渲染機制可以快速找出 State 改變的地方: - 透過 State 的改變,render 出 Virtual DOM - 透過比對 Virtual DOM 的不同,再更新真的 DOM 到畫面 --- ### React渲染頁面的兩個階段 1. 調度階段(reconciliation):在這個階段React 會更新數據生成新的Virtual DOM,然後通過Diff算法,快速找出需要更新的元素,放到更新隊列中去. (負責找出變化的組件) 2. 渲染階段(commit):這個階段React 會遍歷 effect list 執行所有的副作用,期間會執行更新相關的生命週期、掛載 DOM 等等. ( 負責將變化的組件渲染到頁面上) --- ![Reconciliation](https://miro.medium.com/max/910/1*k3cnK3ucFGdE9XtlBV-hQw.png) --- ### 為什麼需要 Virtual DOM? --- 因為操作 DOM 這件事,其實會耗費很大的成本; 而 React 背後運行機制,就是透過比對 Virtual DOM 來避免直接操作 DOM,藉此來提升效能。 除了提升效能這個優點,因為建立 Virtual DOM 這一虛擬層,我們能夠對程式碼進行更多操作,例如: - 轉成真的 DOM 結構 - 轉成 Markdown 語法 - 轉成 mobile APP 語法 - 簡單總結 Virtual DOM 帶來的兩大優點: 在 State 改變時,**透過比對 Virtual DOM 來判斷是否更新、建立真的 DOM,藉此優化效能,可透過 Virtual DOM 建立更多操作** --- ## 如何效能優化 1. key 盡量用後端給出來不易重複的值 2. 使用 Immutable data 3. 優化component 避免 render 4. 使用 suspense 去非同步載入 component 5. dynamic import 三方套件 6. Lazy loading img (Intersection Observer) --- #### key 盡量用後端給出來不易重複的值 在 使用 map 去 展開子節點的時候,要加上key 讓react知道哪個節點才需要做更改 key 不能用index 因為如果順序跟著改變,還是重新 render整個子節點 ```javascript= <li key={item.id}>{item.name}</li> ``` --- #### 使用 Immutable data 剛開始在學 React 的時候,可能會被告誡說如果要更改資料,不能夠這樣寫: ```javascript= // 不能這樣 const newObject = this.state.obj newObject.id = 2; this.setState({ obj: newObject }) // 其實this.state.obj跟newObject還是指向同一個物件, // = call by refenerce // 指向同一塊記憶體,所以當我們在做shallowEqual的時候, // 就會判斷出這兩個東西是相等的,就不會執行 render function 了。 // 也不能這樣 const arr = this.state.arr; arr.push(123); this.setState({ list: arr }) ``` 而是應該要這樣: ```javascript= this.setState({ obj: { ...this.state.obj, id: 2 } }) this.setState({ list: [...this.state.arr, 123] }) ``` 其實使用PureComponent是一件很正常的事情,因為 state 跟 props 如果沒變的話,本來就不該觸發 render function。 而剛剛也提過PureComponent會幫你**shallowEqual** state 跟 props,決定要不要呼叫 render function。 :::warning 如果你已經預期到某個 component 的 props 或是 state 會「很頻繁變動」, 那你根本不用換成 PureComponent,因為你實作之後反而會變得更慢。 ::: --- 如果我們需要更改資料的話怎麼辦呢?你就只能創一個新的。 ```javascript= const obj = { id: 1, text: 'hello' } obj.text = 'world' // 這樣不行,因為你改變了 obj 這個物件 // 你必須要像這樣創造一個新的物件 const newObj = { ...obj, text: 'world' } ``` :::info 有了 Immutable 的概念之後,shallowEqual就不會出錯了,因為如果我們有新的資料,就可以保證它是一個新的 object,這也是為什麼我們在用setState的時候總是要產生一個新的物件,而不是直接對現有的做操作。 ::: --- 這邊還有一個要注意的地方,那就是 spread operator 只會複製第一層的資料而已,它並不是 deep clone: ```javascript= const test = { a: 1, nest: { title: 'hello' } } const copy = {...test} copy.nest === test.nest // true ``` 巢狀的object就要用深拷貝的方式clone JSON.stringify(obj) 以及 JSON.parse(JSONString) 將物件轉成字串再轉成物件,這樣就真的可以確保出來的會是一個新的物件而且是使用不同的記憶體。 ```javascript= let obj1 = {a: {a: 1}} let obj2 = JSON.parse(JSON.stringify(obj1)) obj1.a.a = 2 console.log(obj2.a) // {a: 1} ``` --- ### 優化component 避免 render 透過 React 的 Function Component,我們能將頁面切割成許多 Component 來方便管理。 但需注意這樣的結構,一但 props 或 state 改變時就很容易觸發 re-render(重新渲染),這如果發生在大型專案,不斷重新渲染可能產生效能問題,加重伺服器的負擔。 useCallback memo 與 useCallback 常會搭配使用,useCallback 讓 props 的 Object 在父元件重新渲染時,不重新分配記憶體位址,讓 memo 不會因為重新分配記憶體位址造成渲染。 useMemo 的用法則是無關於父元件,主要用在當元件重新渲染時,減少在元件中複雜的程式重複執行。 :::spoiler 重點整理: * memo() <-- 包住 component * useCallback() * useMemo() ::: --- ### 使用 suspense 去非同步載入 component React 18 透過 <Suspense /> 的幫助,原本頁面可以分割成數個 Component,以 Component 作爲單位來進行 streaming Sever Side Rendering 跟 hydration,提升畫面呈現速度跟可互動速度 ```javascript= import React, {lazy, Suspense} from 'react'; const InputForm = lazy(() => import('../component/InputForm')); const FormPage = () =>{ return <Suspense fallback={<div>Loading...</div>}> <InputForm /> </Suspense>; } export default FormPage; ``` --- before : 當專案規模過大時,bundle.js會很大,導致第一次載入網頁的時間太久 after : render HTML 前就不需獲取所有的資料。 為了避免較大的 bundle size,可以使用 code splitting 的技巧指定部分的 code 不要同步載入。 ![](https://jason-memo.dev/img/react18SSR1-1280w.avif) --- ### dynamic import 三方套件 不是每個網站使用者到網站都會用到每項功能,所以有些套件是不用預先就載入 React Lazy import ```javascript= import React, {lazy} from "react"; const Login = lazy(()=> import("Pages/Login")); ``` Next.js Dynamic import ```javascript= import dynamic from 'next/dynamic' const DynamicMap = dynamic(() => import('react-leaflet/lib/Map')); function Home() { return ( <div> <Header /> <DynamicMap /> <p>HOME PAGE is here!</p> </div> ) } export default Hom ``` --- ### Lazy loading img (Intersection Observer) 1. Intersection Observer API 是一個 async non-blocking API — 能「非同步的」協助觀察一個或多個目標元素進出指定的外層或 viewport 的變化。 2. 取代了使用 scroll 事件監聽,並透過使用getBoundingClientRect()、offsetTop、offsetLeft 等所謂「高成本」的計算,幫助提升效能。 3. 省去原本需要手動透過 getBoundingClientRect() 做計算,Intersection Observer API 非同步的自動計算好讓我們使用 4. 雖然 Intersection Observer API 是一個 async non-blocking API,但需要特別注意的是「callback 函式的執行仍在主執行緒上運行」而沒有非同步的被執行。因此還是要避免在 callback 函式中執行高成本或耗費時間的工作,否則就喪失 Intersection Observer API 所帶來的好處了! 5. 使用在如 lazy loading 的實作上,當工作完後(將資源載入後),就應該透過IntersectionObserver 實例上的 unobserve(目標元素) 方法結束觀察,避免資源的浪費。 經典文章: 1. https://medium.com/%E9%BA%A5%E5%85%8B%E7%9A%84%E5%8D%8A%E8%B7%AF%E5%87%BA%E5%AE%B6%E7%AD%86%E8%A8%98/%E8%AA%8D%E8%AD%98-intersection-observer-api-%E5%AF%A6%E4%BD%9C-lazy-loading-%E5%92%8C-infinite-scroll-c8d434ad218c 2. https://medium.com/@mingjunlu/lazy-loading-images-via-the-intersection-observer-api-72da50a884b7 範例: https://codesandbox.io/s/intersectionobserver-image-br5br --- React 優化效能經典文章 https://medium.com/starbugs/%E4%BB%8A%E6%99%9A-%E6%88%91%E6%83%B3%E4%BE%86%E9%BB%9E-web-%E5%89%8D%E7%AB%AF%E6%95%88%E8%83%BD%E5%84%AA%E5%8C%96%E5%A4%A7%E8%A3%9C%E5%B8%96-e1a5805c1ca2 --- # :100: :muscle: :tada: --- ### 參考資料 - https://www.coderbridge.com/series/d62a3665d90b44278d4be5f843959ec7/posts/257f2587f1c94cdaa9a0cbaf1814e1f3 - https://blog.techbridge.cc/2018/01/05/react-render-optimization/ - https://jigsawye.com/2021/06/10/react-18-new-ssr-architecture - https://hackmd.io/@Heidi-Liu/virtual-dom - https://segmentfault.com/a/1190000021132816 - https://segmentfault.com/a/1190000039682751 --- ### Thank you! :sheep: You can find me on - GitHub : https://github.com/Ken-ChangCK - Linkedin me : https://www.linkedin.com/in/kai-cc-a445221b5/ - Blog: https://ken-changck.github.io/c.c.k