Concurrency Rendering 並發渲染
為什麼要介紹這個機制?因為新功能都是建立在它提供的基礎上。
Concurrency 具體採用的哪些技術,像 priority queues(優先佇列)、multiple buffering(多重緩衝)這些先不談,單純就聊一下它與舊機制的不同之處。在過去,React 採用 Blocking Rendering,特點是同步運作,每次只處理一個渲染任務,一旦任務開始就不會中斷、會一直等到結果呈現在屏幕上。
舊機制問題是,如果 React 要 render 大型任務,那麼期間 UI 會像壞掉一樣、無法及時回應用戶操作,使用者體驗很糟。
而 React 18 採用了 Concurrency 機制,它可以隨時暫停渲染、繼續、或完全放棄正在進行的更新,在背景準備新的螢幕畫面、而不阻塞到主執行緒,即時正在處理大型任務,UI 也能夠立即回應用戶的輸入。
1. Automatic Batching 自動批次處理
先了解什麼是 batching?
React 把多個狀態的更新分配到同一次 re-render 的過程,達到更好的效能
以這段程式碼為例,該按鈕的點擊事件會更新到兩個狀態,但只會觸發一次 re-render,這是因為 React 幫我們做到了批次處理的工作,批次處理就是:
- 避免了不必要的 re-render
- 避免 state 更新到一半可能出現的錯誤(如果多個狀態是連動的,就該一起更新)
在 React 17 就已經有這個功能,但過去只有 React event handlers 可以做到批次處理,如果多狀態的更新是被放在以下幾種情況,就會失效:
- Promises
- setTimeout
- DOM event handlers
點擊後先打 api 拿東西,等 getData 過後才更新狀態,這是 fetch callback,而不是 React Event callback。
於是,React 18 中增加了 Automatic Batching 功能,無論是在哪裡情況下都會自動採用批次處理,包括以前被限制的 timeouts、promises 及原生的 event handlers,提高應用程式的效能。
2. Transitions
React 18 引入一個新概念,用來區別狀態更新的緊急程度,分為 urgent 和 non-urgent 更新。
Urgent updates:提供用戶直接反饋,例如鍵盤打字一秒後才顯示在畫面會被認為是當機,影響體驗
Non-urgent update:用戶通常可預期更新會有一定程度的延遲,比如按鈕按下去到整頁搜尋結果出爐,又稱為 Transitions
在 React 18 裡面我們可以利用 startTransition API 來區分兩者:
包裝在 startTransition
api 中會被視為 non-urgent updates(不緊急的更新),如重新渲染的過程中有出現其他 urgent updates 的操作,那麼過程就會被中斷、拋棄,React 將放棄正在進行的更新,優先回應用戶比較緊急的操作需求。
那麼,為什麼我們會需要把狀態的 updates 區分呢?
一般來說,Transition update,DOM 更新通常比較大,過去沒有做區分的時候,Urgent update 可能會被繁重的 Transition update 效能問題卡住。
以我之前做的電影篩選網站為例,左側有 progress bar 可以拖曳、用來篩選出不同年份的電影,使用者會預期:拖動左側的數字,就會去篩選、並在右邊列表展示該區間的電影。而拖曳這個舉動是所謂的 Urgent update,每拉一點距離就應該反映在介面上,而右側列表可搭配 loading,需要一些時間去打 api 拿資料更新,而列表更新就是一種 Transition。
電影可能有幾千幾百部,這種大型任務渲染很花時間,如果還沒更新完就繼續拖動,progress bar 將不會有任何移動,這就是過去舊機制的問題所在。
為了讓大型 UI 轉換發生時,仍然保持互動速度,我們在 Concurrency 新機制的基礎下,預先告知 React 哪些 updates 是 Transitions、是可以被中斷,如果在重新渲染中有出現 urgent updates(例如輸入新的關鍵字、拖動到新的年份),那 React 將放棄正在進行的更新,和使用者在更新過程繼續互動,直接再去 render 最新版本。
如何使用 transitions
- startTransition:基本 api
- useTransition:用於轉換的 hook,還包括一個追踪 pending 的值
在速度快的裝置,不管哪種更新都幾乎沒有延遲,但是,在速度慢的設備上就會出現差異,儘管某些時候 UI 轉換會嚴重 delay,我們仍可以透過以上方式讓畫面保持互動性。
3. Suspense
在介紹 Suspense 前,需要先聊一下 Code Splitting 概念。
大部分 React Application 會使用像 Webpack 這類工具來 bundle 檔案。Bundle,簡單來說就是將 import 的檔案合併為單一檔案的打包過程,這個打包過的檔會被引入到網頁來載入整個應用程式、產生所有互動與畫面。以下是一個簡單示範,當然實際結果和以下範例不太相同、還會經過壓縮什麼的,不過基本概念就是這樣:
如果引入大量第三方函式庫,將讓 bundle 變得太大,之所以要做「Code Splitting」,目的就是於幫助我們「延遲載入」一些目前還不需要的東西,等需要時,再以「動態的方式要回那個功能」打包的 JavaScript。
如何 Dynamic Import 動態載入、實現 Code Splitting
🟩 第一招:原生 ECMAScript 提供的 Dynamic Import
可以動態引用方法
也能動態引用元件
React.lazy
🟩 第二招:React 原生提供的 第一次要用到元件時,才去自動載入包含該元件的 bundle。而可以動態 import 渲染的 component,以下稱之為 lazy component。
- 接收一個呼叫動態 import() 的 function
- 回傳一個 Promise,resolve 包含 React 元件 的 default export 的 module
通常,因為是要動態載入,多數 lazy component 需要搭配 <Suspense />
一起使用,用意在於,等待 lazy component 載入時,可以先顯示一些 loading 的符號或替代元素(稱為 fallback )。
Suspense 的概念
如果要渲染的元件還沒準備好,先顯示指定的加載元件。概念上來講,Suspend 類似於程式碼裡 catch
的概念,而 catch 的東西是還沒準備好的元件。
instead of catching errors, it catches components "suspending"
以下面這段程式碼為例:
- 當 showComments 從 false -> true
- React 開始渲染
<Panel />
,但其子元件<Comments />
處在 suspending 狀態、仍在準備中 - 因為不能夠顯示
<Panel />
全部內容,所以只能看到 Spinner
過去 React 16.6 早已有這項功能,但它跟最新 React 18 的流程有點不同:
從流程的改變我們可以知道,不夠完整的元件會被 React 拋棄、而不是先塞到 DOM 裡面,這也回呼了前面提到的新機制:
React 現在不是單一的同步運作,它會中斷、甚至放棄正在進行的更新,保持 UI 上一致性
當克服 Server-side rendering 的限制及效能問題
早在 React 16.6 就出現 React.lazy 和 Suspense 功能,不過在工作上其實很少用到,因為它並不支援 SSR,所以在很多專案上不能推行,參考 New Suspense SSR Architecture in React 18,在 server-side 頁面會遇到的效能問題:
- Server 必須先拉取所有需要 api 資料
- Server 拿到全部資料才開始組 HTML
- Client 必須 load 完所有的 js 才能進行 hydration
- Client 必須 hydration 所有的 Component 畫面,才可以互動
下方這張圖展示了 server-side 頁面的模式:
灰色部分代表還沒有 hydration,即應用程序的 JavaScript 還沒全部 load 完,這時你去點擊按鈕不會執行任何操作,但是,對於內容特別繁重的網站來說,SSR 非常有用,因為它讓裝置或網路較差的使用者在加載同時可以先查看內容。
舊版 <Suspense />
無法應用在 server-side rendering 上,是因為無論是渲染 HTML 或進行 hydration,是採用「全部一起」或「全部沒有」,不存在所謂的部分先加載或部分先水合。
但在 React 18,反而是透過 <Suspense />
,將頁面分割成數個 Component,以 Component 爲單位來進行 streaming rendering 跟 selective hydration:
Streaming HTML
- 儘早發出 HTML,不再需要按照順序由上而下依序渲染 HTML
- 誰先傳輸過來、誰就先進行替換
Selective Hydration
- 透過 React.lazy,把原本 script 分割
- 先準備好的 Component,就可以先進行 hydration
- 優先為正在互動的 Component 進行 hydration
以上方案例,來講解 React 18 的 Suspense 模式:
- 評論區
<Comment />
,我們用<Suspend />
包裝起來,即在告訴 React 説不用等待<Comment />
- React 知道不需要等待它後,開始為頁面的其餘部分傳送 HTML。而這裡,React 將傳送
<Spinner />
而不是<Comment />
- 即先顯示 Suspend 指定的 fallback 內容(
<Spinner />
) - 等待一直到
<Comment />
的 data 和 HTML 在服務端準備好 - React 再將 streaming html 跟小 script(標記該 HTML 的正確位置)送到瀏覽器渲染、替換原本的
<Spinner />
與舊有的 HTML 傳輸方式不同,不必再自上而下的順序發生,不用依序、不必再整體水合,streaming rendering 和 selective hydration 允許我們根據 SSR 需求來分割畫面。
官方文件中提到,經過測試,幾乎所有元件都可以適用 Concurrency 新機制,就算升級到 React 18,也可以是在開發新 feature 時選擇性啟用新功能、而不會影響現有的程式碼,真的值得期待!