alibaba / alibaba/ice

[RFC] DataLoader 不阻塞页面渲染

Open
#6,124 3 comments 0 reactions 1 assignee View on GitHub

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

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>
  );
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.