1. Fetch-on-Render
All along, the best practice we've followed is the Fetch-on-Render pattern, that is:
-
When rendering component (render) and discovering no data, first show loading
-
Send request (fetch) at
componentDidMount -
Start rendering data after data comes back
The benefit of doing this is organizing code by concern, data requests and corresponding UI rendering logic are put together. But the disadvantages are also obvious:
-
Serial: The entire process is serial (first render then fetch), leading to deeper data loading later
-
fetch bound to render: Means lazy component's fetch timing is also lazy, components on-demand loading has performance burden
Obviously, data requests can go first, fetch binding to render is also not reasonable. But before starting optimization, consider a question: what goal do we want to achieve?
2. Choice Between Fish and Bear's Paw
In terms of user experience, the effect we want to achieve is:
-
Display most important content as early as possible
-
At the same time, don't want secondary content to slow down entire page (complete content) loading time
Want part of content to display first, yet don't want remaining content to be delayed due to priority. Seems like a choice between fish and bear's paw, but parallelism makes having both possible, corresponding to technical implementation:
-
Data and code should both be incrementally loaded (by importance)
-
And preferably in parallel
Thus, Render-as-You-Fetch pattern emerged
3. Render-as-You-Fetch
Specifically, Render-as-You-Fetch pattern has 4 points:
-
Separate data dependencies: Parallel load data, create views
-
Load data as early as possible: Load data in event handler
-
Incrementally load data: Prioritize loading important data
-
Load code as early as possible: Treat code as data too
First three points target data loading's what, when and how, last point targets view. Because if data is already fast enough, view must also keep up, after all v = f(d)
P.S. For more information about v = f(d), see Deep Dive React
Separate Data Dependencies: Parallel Load Data, Create Views
fetch binding to render leads to data loading's how and when both limited by render, is first major obstacle. So first need to extract data dependencies from view, separate what (data to load) from how (loading method) and when (loading timing):
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.
There are two implementation methods, either manually separate, or rely on build tools to automatically extract:
-
Define same-name files: For example put
MyComponent.jsxcorresponding data request inMyComponent.data.js -
Extract data dependencies at compile time: Data requests still stay in component definition, compiler parses and extracts data dependencies from it
Latter method while separating data dependencies, can also consider component definition's cohesion, is Relay's adopted approach:
// 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 extracts GraphQL data dependencies from components, can even further aggregate, integrating fragmented requests into one Query
Load Data As Early As Possible: Load Data in Event Handler
After separating data and views, both can load in parallel independently, then, what timing to start loading data?
Of course as early as possible, so need to after receiving interaction events (such as click, switch tab, open modal window), simultaneously load code and data separately:
The key is to start fetching code and data for a new view in the same event handler that triggers showing that view.
For page-level data, can hand over to router to uniformly control data loading timing, for example:
// 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;
Can even preload at earlier timing like hover, mousedown etc.:
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.
At this point, can consider concentrating preload capabilities in router or core UI components, because whether preload feature is enabled usually depends on user's device and network situation, centralized management is better to control
Incrementally Load Data: Prioritize Loading Important Data
If data loading timing is already early enough, is there still a way to speed up?
Yes. In terms of experience, we tend to prioritize displaying more important view, without waiting for all data to come back:
But we still want to be able to show more important parts of the view without waiting for all of our data.
For this, Facebook implemented @defer directive in GraphQL:
// 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>
);
}
Stream return data, prioritize providing non-@defer fields, equivalent to data-level Suspense feature. This thinking also applies to REST API, for example group data fields by priority, split into two requests sent in parallel, avoid unimportant data slowing down important data
Load Code As Early As Possible: Treat Code As Data Too
After doing all this, data loading aspect seems to have reached limit
However, another factor not to be ignored is React.lazy only loads (component) code when actually rendering, is code-level Fetch-on-Render:
React.lazy won't start downloading code until the lazy component is actually rendered.
Similarly, can treat code as data too, hand over to router to control code loading timing, not decided by render process
4. Examples
-
Render-as-You-Fetch based on GraphQL: Relay Hooks Example App - GitHub Issues Clone
-
Render-as-You-Fetch based on REST API: Suspense Demo for Library Authors
5. Summary
Key to improving loading speed is load code and data as early as possible, incrementally:
Start loading code and data as early as possible, but without waiting for all of it to be ready.
Specifically divided into 4 points:
-
Separate data dependencies: While loading view (code), parallel load its required data
-
Load data as early as possible: Immediately load data after receiving interaction events, can even predict user behavior, preload view
-
Incrementally load data: Prioritize loading important data, but without affecting secondary data loading speed
-
Load code as early as possible: Treat (component) code as data too, use similar approach to improve its loading speed
No comments yet. Be the first to share your thoughts.