1. 引言
這次介紹的文章是 scheduling-in-react,簡單來說就是 React 的排程系統,為了得到更順滑的使用者體驗。
畢竟前端做到最後,都是體驗優化,前端帶給使用者的價值核心就在於此。
2. 概述
文章從 Dan 在 JSConf 提到的 Demo 說起:
這是一個測試效能的 Demo,隨著輸入框字元的增加,下方圖表展示的資料量會急速提升。在 Synchronous 與 Debounced 模式下的效果都不盡如人意,只有 Concurrent 模式下看起來是順暢的。
那麼為什麼普通的 Demo 會很卡呢?
這就涉及到瀏覽器 Event Loop 規則了。
JS 是單執行緒的,瀏覽器同一時間只能做一件事情,而肉眼能識別的重新整理頻率在 60FPS 左右,這意味著我們需要在 16ms 之內完成 Demo 中的三件事:響應使用者輸入,做動畫,Dom 渲染。
然而目前幾乎所有框架都使用同步渲染模式,這意味著如果一個渲染函式執行時間超過了 16ms,則不可避免的發生卡頓。
總結一下有兩個主要問題:
- 長時間執行的任務造成頁面卡頓,我們需要保證所有任務能在幾毫秒內完成,這樣才能保證頁面的流暢。
- 不同任務優先順序不同,比如響應使用者輸入的任務優先順序就高於動畫。這個很好理解。
React 排程機制
為了解決這個問題,React16 通過 Concurrent(並行渲染) 與 Scheduler(排程)兩個角度解決問題:
- Concurrent: 將同步的渲染變成可拆解為多步的非同步渲染,這樣可以將超過 16ms 的渲染程式碼分幾次執行。
- Scheduler: 排程系統,支援不同渲染優先順序,對 Concurrent 進行排程。當然,排程系統對低優先順序任務會不斷提高優先順序,所以不會出現低優先順序任務總得不到執行的情況。
為了保證不產生阻塞的感覺,排程系統會將所有待執行的回撥函式存在一份清單中,在每次瀏覽器渲染時間分片間儘可能的執行,並將沒有執行完的內容 Hold 住留到下個分片處理。
Concurrent 的正式 API 會在 2019 Q2 釋出,現在可以通過 <React.unstable_ConcurrentMode>
API 方式呼叫:
ReactDOM.render(
<React.unstable_ConcurrentMode>
<App />
</React.unstable_ConcurrentMode>,
rootElement
);
只申明這個是不夠的,因為我們還沒有申明各函式執行的優先順序。我們可以通過 npm i scheduler
包來申明函式的優先順序:
import { unstable_next } from "scheduler";
function SearchBox(props) {
const [inputValue, setInputValue] = React.useState();
function handleChange(event) {
const value = event.target.value;
setInputValue(value);
unstable_next(function() {
props.onChange(value);
sendAnalyticsNotification(value);
});
}
return <input type="text" value={inputValue} onChange={handleChange} />;
}
在 unstable_next()
作用域下的程式碼優先順序是 Normal
,那麼產生的效果是:
- 如果
props.onChange(value)
可以在 16ms 內執行完,則與不使用unstable_next
沒有區別。 - 如果
props.onChange(value)
的執行時間過長,可能這個函式會在下次幾次的 Render 中陸續執行,不會阻塞後續的高優先順序任務。
排程帶來的限制
排程系統也存在兩個問題。
- 排程系統只能有一個,如果同時存在兩個排程系統,就無法保證排程正確性。
- 排程系統能力有限,只能在瀏覽器提供的能力範圍內進行排程,而無法影響比如 Html 的渲染、回收週期。
為了解決這個問題,Chrome 正在與 React、Polymer、Ember、Google Maps、Web Standars Community 共同建立一個 瀏覽器排程規範,提供瀏覽器級別 API,可以讓排程控制更底層的渲染時機,也保證排程器的唯一性。
3. 精讀
關於 React 排程系統的剖析,可以讀 深入剖析 React Concurrent 這篇文章,感謝我們團隊的 淡蒼 提供。
簡單來說,一次 Render 一般涉及到許多子節點,而 Fiber 架構在 Render 階段可以暫停,一個一個節點的執行,從而實現了排程的能力。
React 排程能力的限制
這意味著,如果你的 React 應用目前是流暢的,開啟 Concurrent 並不會對你的應用帶來效能體驗上的提升,如果你的 React 應用目前是卡頓的,或者在某些場景下是卡頓的,那麼 Concurrent 或許可以挽救你一下,帶來一些改變。
正如《深入剖析 React Concurrent》一文提到的,如果你的應用沒有效能問題,就不要指望 React 排程能力有所幫助了。
這也是在說,如果一段程式碼邏輯不存在效能問題,就不需要使用 Concurrent 優化,因為這種優化是無效的。我們需要能分辨哪些邏輯需要優化,哪些邏輯不要。
從現在開始嘗試 Function Component
為了配合 React Schedule 的實現,學會使用 Function Component 模式編寫元件是很重要的,因為:
- Class Component 的生命週期概念阻礙了 React 排程系統對任務的拆分。
- 排程系統可能對
componentWillMount
重複呼叫,使得 Class Component 模式下很容易寫出錯誤的程式碼。 - Function Component 遵循了更嚴格的副作用分離,這使得 Concurrent 執行過程不會引發意外效果。
React.lazy
與 Concurrent 一起釋出的,還有 React 元件動態 import 與載入方案。正常的元件載入是這樣的:
import OtherComponent from "./OtherComponent";
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
但如果使用了 import()
動態載入,可以使用 React.lazy
讓動態引入的元件像普通元件一樣被使用:
const OtherComponent = React.lazy(() => import("./OtherComponent"));
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
如果要加入 Loading,就可以配合 Suspense
一起使用:
import React, { lazy, Suspense } from "react";
const OtherComponent = lazy(() => import("./OtherComponent"));
function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
);
}
和 Concurrent 類似,React.lazy 方案也是一種對效能有益的元件載入方案。
排程分類
排程分 4 個等級:
- Immediate:立即執行,最高優先順序。
- render-blocking:會阻塞渲染的優先順序,優先順序類似
requestAnimationFrame
。如果這種優先順序任務不能被執行,就可能導致 UI 渲染被 block。 - default:預設優先順序,普通的優先順序。優先順序可以理解為
setTimeout(0)
的優先順序。 - idle:比如通知等任務,使用者看不到或者不在意的。
目前建議的 API 類似如下:
function mytask() {
...
}
myQueue = TaskQueue.default("render-blocking")
先建立一個執行佇列,並設定佇列的優先順序。
taskId = myQueue.postTask(myTask, <list of args>);
再提交佇列,拿到當前佇列的執行 id,通過這個 id 可以判斷佇列何時執行完畢。
myQueue.cancelTask(taskId);
必要的時候可以取消某個函式的執行。
4. 總結
隨著 Hooks 的釋出,即將到來的 Concurrent 與 Suspense 你是否準備好了呢?
筆者希望大家一起思考,這三種 API 會給前端開發帶來什麼樣的改變?歡迎留言!
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
special Sponsors
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)