一.Fetch-on-Render
一直以来,我们所遵从的最佳实践都是 Fetch-on-Render 模式,即:
渲染组件(render)时发现没有数据,就先显示 loading
componentDidMount
时发送请求(fetch)数据回来之后开始渲染数据
这样做的好处在于按关注点组织代码,数据请求和数据对应的 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 流程来决定
四.示例
基于 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
增量加载数据:优先加载重要数据,但又不影响次要数据的加载速度
尽早加载代码:把(组件)代码也看成数据,通过类似的方式来提升其加载速度