跳到主要内容
黯羽轻扬每天积累一点点

Render-as-You-Fetch

免费2026-03-05#JS#react并行加载数据#react渲染流程优化#react Render-as-You-Fetch#react数据加载最佳实践#渲染时并行加载数据

一.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

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

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

参考资料

评论

暂无评论,快来发表你的见解吧

提交评论