ice [RFC] DataLoader 不阻塞页面渲染

6psbrbz9  于 6个月前  发布在  其他
关注(0)|答案(3)|浏览(61)

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

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

vecaoik11#

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

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

uqzxnwby

uqzxnwby2#

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

like unblocking? or streaming? any words else?

ifsvaxew

ifsvaxew3#

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

like unblocking? or streaming? any words else?

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

相关问题