Travel in Time.

React 18:三大新功能介紹

Published on

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 原生提供的 React.lazy

第一次要用到元件時,才去自動載入包含該元件的 bundle。而可以動態 import 渲染的 component,以下稱之為 lazy component。

  1. 接收一個呼叫動態 import() 的 function
  2. 回傳一個 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"

以下面這段程式碼為例:

  1. 當 showComments 從 false -> true
  2. React 開始渲染 <Panel />,但其子元件 <Comments /> 處在 suspending 狀態、仍在準備中
  3. 因為不能夠顯示 <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 頁面會遇到的效能問題:

  1. Server 必須先拉取所有需要 api 資料
  2. Server 拿到全部資料才開始組 HTML
  3. Client 必須 load 完所有的 js 才能進行 hydration
  4. 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 模式:

  1. 評論區 <Comment />,我們用 <Suspend /> 包裝起來,即在告訴 React 説不用等待 <Comment />
  2. React 知道不需要等待它後,開始為頁面的其餘部分傳送 HTML。而這裡,React 將傳送 <Spinner /> 而不是 <Comment />
  3. 即先顯示 Suspend 指定的 fallback 內容(<Spinner />
  4. 等待一直到 <Comment /> 的 data 和 HTML 在服務端準備好
  5. React 再將 streaming html 跟小 script(標記該 HTML 的正確位置)送到瀏覽器渲染、替換原本的 <Spinner />

與舊有的 HTML 傳輸方式不同,不必再自上而下的順序發生,不用依序、不必再整體水合,streaming rendering 和 selective hydration 允許我們根據 SSR 需求來分割畫面。

官方文件中提到,經過測試,幾乎所有元件都可以適用 Concurrency 新機制,就算升級到 React 18,也可以是在開發新 feature 時選擇性啟用新功能、而不會影響現有的程式碼,真的值得期待!


參考資料