一.Fetch-on-Render
これまで、私たちが従ってきたベストプラクティスは Fetch-on-Render モードでした。つまり:
-
コンポーネントをレンダリング(render)する際にデータがない場合、まず loading を表示
-
componentDidMount時にリクエストを送信(fetch) -
データが戻ってきた後、データのレンダリングを開始
このようにする利点は関心事ごとにコードを整理できることで、データリクエストとデータに対応する UI レンダリングロジックを一緒に配置できます。しかし欠点も明確です:
-
シリアル:プロセス全体がシリアル(まず render 後 fetch)であり、より深層のデータほど遅くロードされる
-
fetch と render のバインド:lazy コンポーネントの fetch タイミングも lazy されることを意味し、コンポーネントの必要に応じたロードにパフォーマンス負担が生じる
明らかに、データリクエストは先行可能で、fetch と render のバインドも合理的ではありません。しかし最適化を始める前に、1 つの問題を検討しましょう:私たちが実現したい目標は何でしょうか?
二.魚と熊掌の選択
ユーザー体験に関して、私たちが達成したい効果は:
-
できるだけ早く最も重要なコンテンツを表示
-
同時に、二次的なコンテンツがページ全体(完全なコンテンツ)のロード時間を遅くすることも望まない
一部のコンテンツを優先表示したいが、残りのコンテンツが優先度によって遅延表示されることも望まない。これは魚と熊掌の選択のように思えますが、並行性により両立が可能になりました。技術実装に対応すると:
-
データとコードはすべて(重要度に応じて)増分ロードすべき
-
しかも並行が望ましい
そこで、Render-as-You-Fetch モードが現れました
三.Render-as-You-Fetch
具体的には、Render-as-You-Fetch モードは 4 点に分かれます:
-
データ依存の分離:データとビューの並行ロード
-
データの早期ロード:イベントハンドラ内でデータをロード
-
データの増分ロード:重要なデータを優先ロード
-
コードの早期ロード:コードもデータと見なす
前 3 点はデータロードの 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.
2 つの実装方法があり、手動で分離するか、構築ツールに自動抽出させるか:
-
同名ファイルを定義:例えば
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 データ依存を抽出し、さらに集約して、細かなリクエストを 1 つの 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 にも適用可能で、例えばデータフィールドを優先度ごとにグループ化し、2 つのリクエストに分割して並行送信し、重要でないデータが重要なデータを遅くするのを回避 します
コードの早期ロード:コードもデータと見なす
これらすべてを行った後、データロード面ではすでに極限に達したように思えます
しかし、もう 1 つ無視できない要因は 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 をプリロードすることも可能
-
データの増分ロード:重要なデータを優先ロードし、同時に二次的なデータのロード速度にも影響しない
-
コードの早期ロード:(コンポーネント)コードもデータと見なし、同様の方式を通じてロード速度を向上
コメントはまだありません