一。Fetch-on-Render
一直以來,我們所遵從的最佳實踐都是 Fetch-on-Render 模式,即:
-
渲染組件(render)時發現沒有數據,就先顯示 loading
-
componentDidMount時發送請求(fetch) -
數據回來之後開始渲染數據
這樣做的好處在於按關注點組織代碼,數據請求和數據對應的 UI 渲染邏輯放在一塊兒。但缺點也很明顯:
-
串行:整個過程是串行的(先 render 後 fetch),導致越深層的數據越晚加載
-
fetch 與 render 綁定:意味著 lazy 組件的 fetch 時機也被 lazy 了,組件按需加載有了性能負擔
顯然,數據請求能夠先行,fetch 與 render 綁定也並不合理。但在開始優化之前,先考慮一個問題:我們想要實現的目標是什麼?
二。魚和熊掌的抉擇
就用戶體驗而言,我們想要達到的效果是:
-
盡早顯示最重要的內容
-
同時也不希望次要內容拖慢整頁(完整內容)加載時間
既要一部分內容優先展示,又不希望其餘內容因為優先級而延遲展示。似乎是個魚和熊掌的抉擇,但並行性讓二者兼得成為了可能,對應到技術實現上:
-
數據和代碼都應該(按重要程度)增量加載
-
而且最好並行
於是,Render-as-You-Fetch 模式出現了
三。Render-as-You-Fetch
具體的,Render-as-You-Fetch 模式分為 4 點:
-
分離數據依賴:並行加載數據、創建視圖
-
盡早加載數據:在事件處理函數中加載數據
-
增量加載數據:優先加載重要數據
-
盡早加載代碼:把代碼也看成數據
前三點針對數據加載的 what、when 與 how,最後一點針對 view。因為如果 data 已經足夠快了,view 也要跟上,畢竟 v = f(d) 嘛
P.S. 關於 v = f(d) 的更多信息,見 深入 React
分離數據依賴:並行加載數據、創建視圖
fetch 與 render 綁定,導致數據加載的 how 與 when 都受限於 render,是第一大阻礙因素。所以先要把數據依賴從 view 中抽離出來,把 what(要加載的數據)與 how(加載方式)和 when(加載時機)分開:
The key is that regardless of the technology we're using to load our data — GraphQL, REST, etc — we can separate what data to load from how and when to actually load it.
有兩種實現方式,要麼人工分離,要麼靠構建工具來自動提取:
-
定義同名文件:比如把
MyComponent.jsx對應的數據請求放在MyComponent.data.js中 -
編譯時提取數據依賴:數據請求還放在組件定義中,由編譯器來解析提取其中的數據依賴
後者在分離數據依賴的同時,還能兼顧組件定義的內聚性,是 Relay 所採用的做法:
// Post.js
function Post(props) {
// Given a reference to some post - `props.post` - *what* data
// do we need about that post?
const postData = useFragment(graphql`
fragment PostData on Post @refetchable(queryName: "PostQuery") {
author
title
# ... more fields ...
}
`, props.post);
// Now that we have the data, how do we render it?
return (
<div>
<h1>{postData.title}</h1>
<h2>by {postData.author}</h2>
{/* more fields */}
</div>
);
}
由 Relay Compiler 把組件中的 GraphQL 數據依賴提取出來,甚至還能進一步聚合,把細碎的請求整合成一條 Query
盡早加載數據:在事件處理函數中加載數據
數據和視圖分開之後,二者可以並行獨立加載,那麼,什麼時機開始加載數據呢?
當然是盡可能早,所以要在接到交互事件(比如點擊、切換 tab、打開模態窗)後,同時分頭加載代碼和數據:
The key is to start fetching code and data for a new view in the same event handler that triggers showing that view.
對於頁面級數據,可以交給路由統一控制數據加載時機,例如:
// Manually written logic for loading the data for the component
import PostData from './Post.data';
const PostRoute = {
// a matching expression for which paths to handle
path: '/post/:id',
// what component to render for this route
component: React.lazy(() => import('./Post')),
// data to load for this route, as function of the route
// parameters
prepare: routeParams => {
const postData = preloadRestEndpoint(
PostData.endpointUrl,
{
postId: routeParams.id,
},
);
return { postData };
},
};
export default PostRoute;
甚至還可以在 hover、mousedown 之類的更早時機進行預加載:
If we can load code and data for a view after the user clicks, we can also start that work before they click, getting a head start on preparing the view.
此時,可以考慮把預加載能力集中到 router 或核心 UI 組件中,因為預加載特性是否開啟通常取決於用戶的設備和網絡情況,集中管理更好控制
增量加載數據:優先加載重要數據
如果數據加載時機已經足夠早了,還有辦法加快速度嗎?
有。體驗上,我們傾向於優先展示更重要的 view,而不等所有數據都回來:
But we still want to be able to show more important parts of the view without waiting for all of our data.
為此,Facebook 在 GraphQL 中實現了 @defer 指令:
// Post.js
function Post(props) {
const postData = useFragment(graphql`
fragment PostData on Post {
author
title
# fetch data for the comments, but don't block on it being ready
...CommentList @defer
}
`, props.post);
return (
<div>
<h1>{postData.title}</h1>
<h2>by {postData.author}</h2>
{/* @defer pairs naturally with <Suspense> to make the UI non-blocking too */}
<Suspense fallback={<Spinner/>}>
<CommentList post={postData} />
</Suspense>
</div>
);
}
流式返回數據,優先提供非 @defer 字段,相當於數據層面的 Suspense 特性。這種思路同樣適用於 REST API,比如將數據字段按優先級分組,拆成兩個請求並行發送,避免不重要的數據拖慢重要數據
盡早加載代碼:把代碼也看成數據
做完所有的這一切,數據加載方面似乎已經達到極限了
然而,另一個不容忽視的因素是 React.lazy 只在實際渲染時才加載(組件)代碼,是代碼層面的 Fetch-on-Render:
React.lazy won't start downloading code until the lazy component is actually rendered.
類似的,可以把代碼也看成數據,交給路由來控制代碼加載時機,而不由 render 流程來決定
四。示例
-
基於 GraphQL 的 Render-as-You-Fetch:Relay Hooks Example App - GitHub Issues Clone
-
基於 REST API 的 Render-as-You-Fetch:Suspense Demo for Library Authors
五。總結
提升加載速度的關鍵在於盡早、增量地加載代碼和數據:
Start loading code and data as early as possible, but without waiting for all of it to be ready.
具體分為 4 點:
-
分離數據依賴:在加載 view(代碼)的同時,並行加載其所需數據
-
盡早加載數據:接到交互事件後立即加載數據,甚至還能預判用戶行為,預加載 view
-
增量加載數據:優先加載重要數據,但又不影響次要數據的加載速度
-
盡早加載代碼:把(組件)代碼也看成數據,通過類似的方式來提升其加載速度
暫無評論,快來發表你的看法吧