Skip to main content

Render-as-You-Fetch

Free2019-12-01#JS#react并行加载数据#react渲染流程优化#react Render-as-You-Fetch#react数据加载最佳实践#渲染时并行加载数据

Why is Render-as-You-Fetch faster?

1. Fetch-on-Render

All along, the best practice we've followed is the Fetch-on-Render pattern, that is:

  1. When rendering component (render) and discovering no data, first show loading

  2. Send request (fetch) at componentDidMount

  3. 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.jsx corresponding data request in MyComponent.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

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

Reference Materials

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment