Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] DataLoader 不阻塞页面渲染 #6124

Open
chenjun1011 opened this issue Mar 30, 2023 · 3 comments
Open

[RFC] DataLoader 不阻塞页面渲染 #6124

chenjun1011 opened this issue Mar 30, 2023 · 3 comments
Assignees
Labels

Comments

@chenjun1011
Copy link
Collaborator

chenjun1011 commented Mar 30, 2023

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 的组件,数据在加载过程中,整个组件是不渲染的,而我们需要的组件内不依赖数据的部分先行渲染

image

https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md#resuming-a-suspended-component-by-replaying-its-execution

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>
  );
}
@chenjun1011 chenjun1011 self-assigned this Mar 30, 2023
@chenjun1011 chenjun1011 added the need review Need Review label Mar 30, 2023
@wssgcg1213
Copy link
Collaborator

wssgcg1213 commented Apr 6, 2023

useAyncData() 方法是必须的吗, 消费时直接复用 useData() 方法可以吗?

async 这个行为是在渲染之前就决定了 (defer), 渲染一直是同步函数, hooks 的习惯就是多次渲染, 所以理论上不需要再有一个 useAyncData 了

@wssgcg1213
Copy link
Collaborator

wssgcg1213 commented Apr 6, 2023

另外 defer 是推迟, 这个语义上貌似有问题

like unblocking? or streaming? any words else?

@chenjun1011
Copy link
Collaborator Author

另外 defer 是推迟, 这个语义上貌似有问题

like unblocking? or streaming? any words else?

这里 defer 参照的是 react-router 的配置,猜想这里的 defer 应该是跟 script 标签中的 defer 含义类似,先触发加载,但不阻塞渲染

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants