Render-as-You-Fetch

一.Fetch-on-Render

一直以来,我们所遵从的最佳实践都是 Fetch-on-Render 模式,即:

  1. 渲染组件(render)时发现没有数据,就先显示 loading

  2. componentDidMount时发送请求(fetch)

  3. 数据回来之后开始渲染数据

这样做的好处在于按关注点组织代码,数据请求和数据对应的 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 流程来决定

四.示例

五.总结

提升加载速度的关键在于尽早、增量地加载代码和数据

Start loading code and data as early as possible, but without waiting for all of it to be ready.

具体分为 4 点:

  • 分离数据依赖:在加载 view(代码)的同时,并行加载其所需数据

  • 尽早加载数据:接到交互事件后立即加载数据,甚至还能预判用户行为,预加载 view

  • 增量加载数据:优先加载重要数据,但又不影响次要数据的加载速度

  • 尽早加载代码:把(组件)代码也看成数据,通过类似的方式来提升其加载速度

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code