[RFC] DataLoader 不阻塞页面渲染
Open
@chenjun1011 is already working on this.
Since Mar 30, 2023.
need review
rfc
- Dominant language
- TypeScript
- Stars
- 18.6k
- Forks
- 2.1k
- PR merge metrics
- No merged PRs in 30d
Description
Summary | 概述
数据加载异步化,不阻塞渲染,从而减少页面白屏时间
Motivation | 背景
DataLoader 提供了前置加载数据请求的能力,目前的设计是页面需要等待 DataLoader 的数据加载完成后,才开始渲染
如果接口过慢,可能导致白屏时间过久
因此,希望也能提供 DataLoader 不阻塞渲染的能力,页面可以先渲染,数据加载完成后,再次更新渲染内容
Usage example | 使用示例
见方案设计
Detailed design | 方案设计
根据对数据消费方式的差异,分为两套方案
方案1:
声明式的方式
- 在 defineDataLoader 时,通过 defer 标记数据请求不阻塞渲染
- 在消费数据时,使用 Suspense + Await 组件,声明数据请求各阶段的渲染内容
import { useData, defineDataLoader, Await } from 'ice';
import * as React from 'React';
export default function Home() {
const data = useData();
return (
<main>
<h1>Let's locate your package</h1>
<React.Suspense
fallback={<p>Loading package location...</p>}
>
<Await
resolve={data}
errorElement={
<p>Error loading package location!</p>
}
>
{(packageLocation) => (
<p>
Your package is at {packageLocation.latitude}
lat and {packageLocation.longitude} long.
</p>
)}
</Await>
</React.Suspense>
</main>
);
}
export const dataLoader = defineDataLoader(async () => {
const packageLocationPromise = getPackageLocation(
params.packageId,
);
return packageLocationPromise;
}, {
defer: true,
});
方案2:
进行状态判断
- 在 defineDataLoader 时,通过 defer 标记数据请求不阻塞渲染
- 使用 useAsyncData 来获取数据,返回值包含 3 项内容来标记请求状态,业务可通过条件判断来返回不同的内容
- data 真实数据
- error 请求的错误信息
- isLoading 是否还在请求中
import { useAsyncData, defineDataLoader } from 'ice';
export default function Home() {
const { data, error, isLoading } = useAsyncData();
if (error) {
return <div>failed to load</div>;
}
if (isLoading) {
return <div>loading...</div>;
}
return (
<main>
<h1>Let's locate your package</h1>
<p>
Your package is at {data.latitude}
lat and {data.longitude} long.
</p>
</main>
);
}
export const dataLoader = defineDataLoader(async () => {
const packageLocationPromise = getPackageLocation(
params.packageId,
);
return packageLocationPromise;
}, {
defer: true,
});
如果 defineDataLoader 传入的是数组,useAsyncData 的返回值也对应一个数组,数组中的每一项状态同上
const [data1, data2] = useAsyncData();
const { data, error, isLoading } = data1;
Additional context | 额外信息
两个方案对比
方案1
- 同 react-router、remix 中的用法
- 优势是
- 声明式的,可以填空式的去补齐 error 状态、loading 状态下的 ui 展现,避免遗漏处理某些状态
- 不再 Suspense 内的组件,首次可以正常渲染
- 可以复用 react-router 的现有能力
方案2
- 同 react-query、 swc 中的写法
- 优势是
- 代码精简
- 如果 Route 中有多处同时依赖数据,只需要各自判断数据是否存在,不用都包裹 Suspense
其他需要考虑的点是:
- SSR 需要对齐用法,如果用了 useAsyncData,SSR 下返回的数据格式需要保持一致
- 加上非阻塞的模式和流式的模式,数据请求的方式一共有 3 种了,是否概念太多,如果是 方案一,是否需要将流式的用法统一过去
和 react use 的区别
使用了 react use 的组件,数据在加载过程中,整个组件是不渲染的,而我们需要的组件内不依赖数据的部分先行渲染
async function fetchTodo(id) {
const data = await fetchDataFromCache(`/api/todos/${id}`);
return {contents: data.contents};
}
function Todo({id, isSelected}) {
const todo = use(fetchTodo(id));
return (
<div className={isSelected ? 'selected-todo' : 'normal-todo'}>
{todo.contents}
</div>
);
}
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.