Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: modify prefetch doc
Browse files Browse the repository at this point in the history
nyqykk committed Sep 19, 2024
1 parent c0aba79 commit b4c2573
Showing 13 changed files with 23,736 additions and 20,150 deletions.
5 changes: 0 additions & 5 deletions apps/website-new/docs/en/guide/_meta.json
Original file line number Diff line number Diff line change
@@ -13,10 +13,5 @@
"type": "dir",
"name": "framework",
"label": "Frameworks"
},
{
"type": "dir",
"name": "performance",
"label": "Performance"
}
]
288 changes: 0 additions & 288 deletions apps/website-new/docs/en/guide/performance/prefetch.mdx

This file was deleted.

5 changes: 5 additions & 0 deletions apps/website-new/docs/en/practice/_meta.json
Original file line number Diff line number Diff line change
@@ -20,5 +20,10 @@
"name": "scenario",
"label": "Scenarios",
"collapsed": true
},
{
"type": "dir",
"name": "performance",
"label": "Performance"
}
]
287 changes: 287 additions & 0 deletions apps/website-new/docs/en/practice/performance/prefetch.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
# Data Prefetch

:::warning
This feature is not currently supported by Rspack producer projects
:::

## What is Data Prefetch
Data Prefetch can advance the remote module interface request to be sent immediately after `remoteEntry` is loaded, without waiting for component rendering, thus improving the first screen speed.

## Applicable scenarios
The pre-process of the first screen of the project is long (for example, authentication and other operations are required) or the scenario where the data is expected to be directly output on the second screen

- Conventional loading process of hosts:

`Host HTML`(hosts HTML) -> `Host main.js`(hosts entry js) -> `Host fetch`(hosts authentication and other pre-actions) -> `Provider main.js`(producer entry js) -> `Provider fetch`(producer sends request)
![](@public/guide/performance/data-prefetch/common.jpg)
- Loading process after using Prefetch
![](@public/guide/performance/data-prefetch/prefetch.jpg)
- Call `loadRemote` in advance in the pre-process (loadRemote will send the producer interface request with prefetch requirements synchronously)
![](@public/guide/performance/data-prefetch/advanced-prefetch.jpg)

You can see that the producer's request is advanced to the root part js in parallel. **Currently, the optimization effect of the first screen depends on the project loading process. Theoretically, the second screen can be directly output by calling `loadRemote` in advance**, which can greatly improve the overall rendering speed of the module when the front process is long

## Usage
1. Install the `@module-federation/enhanced` package for `producer` and `hosts`

import { Tab, Tabs } from '@theme';

<Tabs values={[{ label: "npm" },{ label: "yarn" },{ label: "pnpm" }]}>
<Tab>
```bash
npm install @module-federation/enhanced
```
</Tab>
<Tab>
```bash
yarn add @module-federation/enhanced
```
</Tab>
<Tab>
```bash
pnpm add @module-federation/enhanced
```
</Tab>
</Tabs>

2. Add a `.prefetch.ts(js)` file to the producer's expose module directory. If there is the following `exposes`
``` ts title=rsbuild(webpack).config.ts
new ModuleFederationPlugin({
exposes: {
'.': './src/index.tsx',
'./Button': './src/Button.tsx',
},
// ...
})
```
At this time, the producer project has two `exposes`, `.` and `./Button`,
then we can create two prefetch files, `index.prefetch.ts` and `Button.prefetch.ts`, under `src`, taking `Button` as an example

**Note that the exported function must be exported as default or Prefetch It will be recognized as a Prefetch function only when it ends with default export or Prefetch (case insensitive)**
``` ts title=Button.prefetch.ts
// Here, the defer API provided by react-router-dom is used as an example. Whether to use this API can be determined according to needs. Refer to the question "Why use defer, Suspense, and Await components"
// Users can install this package through npm install react-router-dom
import { defer } from 'react-router-dom';

const defaultVal = {
data: {
id: 1,
title: 'A Prefetch Title',
}
};

// Note that the exported function must end with default export or Prefetch to be recognized as a Prefetch function (case insensitive)
export default (params = defaultVal) => defer({
userInfo: new Promise(resolve => {
setTimeout(() => {
resolve(params);
}, 2000);
})
})
```

Use in `Button`
```tsx title=Button.tsx
import { Suspense } from 'react';
import { usePrefetch } from '@module-federation/enhanced/prefetch';
import { Await } from 'react-router-dom';

interface UserInfo {
id: number;
title: string;
};
const reFetchParams = {
data: {
id: 2,
title: 'Another Prefetch Title',
}
}
export default function Button () {
const [prefetchResult, reFetchUserInfo] = usePrefetch<UserInfo>({
// Corresponds to (name + expose) in producer ModuleFederationPlugin, for example, `app2/Button` is used for consumption `Button.prefetch.ts`
id: 'app2/Button',
// Optional parameter, required after using defer
deferId: 'userInfo',
// default export does not need to pass functionId by default, here is an example, if it is not default export, you need to fill in the function name,
// functionId: 'default',
});

return (
<>
<button onClick={() => reFetchUserInfo(reFetchParams)}>Resend request with parameters</button>
<Suspense fallback={<p>Loading...</p>}>
<Await
resolve={prefetchResult}
children={userInfo => (
<div>
<div>{userInfo.data.id}</div>
<div>{userInfo.data.title}</div>
</div>
)}
></Await>
</Suspense>
</>
)
};
```

3. Set `dataPrefetch: true` in the producer's ModuleFederationPlugin configuration
```ts
new ModuleFederationPlugin({
// ...
dataPrefetch: true
}),
```
This completes the interface pre-request. After `Button` is used by the hosts, the interface request will be sent out in advance (it will be sent when the js resource is loaded, and the normal project needs to wait until the component is rendered).
In the above example, `Button` will first render `loading...`, and then display the data after 2s
Click `Resend request with parameters` to re-trigger the request and add parameters to update the component

## View the optimization effect
Open the log mode in the browser console to view the output (it is best to use the browser cache mode to simulate the user scenario, otherwise the data may be inaccurate)
The default optimization effect is data 3 minus data 1 (simulating the user sending a request in `useEffect`). If your request is not sent in `useEffect`, you can manually call `performance.now()` at the interface execution. to subtract data 1
``` ts
localStorage.setItem('FEDERATION_DEBUG', 1)
```
![](@public/guide/performance/data-prefetch/log.jpg)

## API
### usePrefetch
#### Function
- Used to obtain pre-request data results and control re-initiated requests

#### Type
``` ts
type Options: <T>{
id: string; // Required, corresponding to (name + expose) in the producer MF configuration, for example, `app2/Button` is used to consume `Button.prefetch.ts`.
functionId?: string; // Optional (default is default), used to get the name of the function exported in the .prefetch.ts file, the function needs to end with Prefetch (case insensitive)
deferId?: string; // Optional (required after using defer), after using defer, the function return value is an object (there can be multiple keys in the object corresponding to multiple requests), deferId is a key in the object, used to get the specific request
cacheStrategy?: () => boolean; // Optional, generally not manually managed, managed by the framework, used to control whether to update the request result cache, currently after the component is uninstalled or the reFetch function is manually executed, the cache will be refreshed
} => [
Promise<T>,
reFetch: (refetchParams?: refetchParams) => void, // Used to re-initiate a request, often used in scenarios where the interface needs to re-request data after the internal state of the component is updated. Calling this function will re-initiate a request and update the request result cache
];

type refetchParams: any; // Used to re-initiate requests with parameters in components
```

#### Usage
``` ts
import { Suspense } from 'react';
import { usePrefetch } from '@module-federation/enhanced/prefetch';
import { Await } from 'react-router-dom';

export const Button = () => {
const [userInfoPrefetch, reFetchUserInfo] = usePrefetch<UserInfo>({
// Corresponds to (name + expose) in the producer MF configuration, for example, `app2/Button` is used to consume `Button.prefetch.ts`
id: 'app2/Button',
// Optional parameter, required after using defer
deferId: 'userInfo'
// default export does not need to pass functionId by default, here is an example, if it is not default export, you need to fill in the function name,
// functionId: 'default',
});

return (
<>
<button onClick={() => reFetchUserInfo(reFetchParams)}>Resend request with parameters</button>
<Suspense fallback={<p>Loading...</p>}>
<Await
resolve={prefetchResult}
children={userInfo => (
<div>
<div>{userInfo.data.id}</div>
<div>{userInfo.data.title}</div>
</div>
)}
></Await>
</Suspense>
<>
)
}
```

### loadRemote
#### Function
If the user manually calls [loadRemote](/zh/guide/basic/runtime.html#loadremote) in the hosts project API, then it will be considered that the hosts not only wants to load the producer's static resources, but also wants to send the interface request in advance, which can make the project render faster. This is especially effective in the scenario where **the first screen has a pre-request** or **you want the second screen to be directly displayed**. It is suitable for the scenario where the second screen module is loaded in advance on the current page.
#### How to use
``` ts
import { loadRemote } from '@module-federation/enhanced/runtime';

loadRemote('app2/Button');
```

#### Note
Note that this may cause data caching problems, that is, the producer will give priority to the pre-requested interface results (the user may have modified the server data through interactive operations). In this case, outdated data will be used for rendering. Please use it reasonably according to the project situation.

## Questions and Answers

### 1. Is there any difference with [Data Loader](https://reactrouter.com/en/main/route/loader) of React Router v6?
React Router's Data Loader can only be used for single projects, that is, it cannot be reused across projects. At the same time, Data Loader is bound by route, not by module (expose). Data Prefetch is more suitable for remote loading scenarios.

### 2. Why use defer, Suspense, and Await components? [Reference link](https://reactrouter.com/en/main/guides/deferred)
Defer and Await components are APIs and components provided by React Router v6 for data loading and rendering loading. The two are usually used with React's Suspense to complete the process of rendering loading -> rendering content. You can see that defer returns an object. When the Prefetch function is executed, all requests corresponding to the keys in this object (that is, value) will be sent out at once, and defer will track the status of these Promises, cooperate with Suspense and Await to complete the rendering, and these requests will not block the rendering of the component (loading... will be displayed before the component rendering is completed)

### 3. Can I not use defer, Suspense and Await?

Yes, but if there is a blocking function execution operation in the export function (for example, there is await or the return is a Promise), the component will wait for the function to complete before rendering. Even if the component content has been loaded, the component may still wait for the interface to complete before rendering, for example
``` ts
export default (params) => (
new Promise(resolve => {
setTimeout(() => {
resolve(params);
}, 2000);
})
)
```
### 4. Why not defer everything by default?
Make the developer scenario more controllable. In some scenarios, developers prefer users to see the entire page at once, rather than rendering loading. This allows developers to better weigh the scenarios. [Reference](https://reactrouter.com/en/main/guides/deferred#why-not-defer-everything-by-default)

### 5. Can Prefetch carry parameters?
For the first request, since the request time and js resource loading are parallel, it does not support passing parameters from within the component. You can manually set the default value. And usePrefetch will return the reFetch function, which is used to resend the request to update data within the component. At this time, it can carry parameters

### 6. How to operate the business to minimize cost transformation?
1. Put the interface that needs to be prefetched in .prefetch.ts
2. The prefetch function is wrapped with `defer` to return an object (you can also return an object directly. If you return a value, it will be blocked by the component js loading await)
3. Business components generally send requests in `useEffect`:
``` ts Button.tsx
import { useState } from 'react';
import { usePrefetch } from '@module-federation/enhanced/prefetch';

export const Button = () => {
const [state, setState] = useState(defaultVal);
const [userInfoPrefetch, reFetchUserInfo] = usePrefetch<UserInfo>({
// Corresponds to (name + expose) in the producer MF configuration, for example, `app2/Button` is used to consume `Button.prefetch.ts`
id: 'app2/Button',
// Optional parameter, required after using defer
deferId: 'userInfo',
// default export does not need to pass functionId by default, here is an example, if it is not default export, you need to fill in the function name,
// functionId: 'default',
});

useEffect(() => {
// General scenario usually sends a request here
userInfoPrefetch
.then(data => (
// Update data
setState(data)
));
}, []);

return (
<>{state.defaultVal}<>
)
}
```

### 7. Why does the Prefetch function I defined not work?
Note that the exported function must end with default export or Prefetch to be recognized as a Prefetch function (case insensitive)

### 8. Can the module optimize performance in the secondary screen?
Yes, and the effect is quite obvious. Since Data Prefetch is for all expose modules, the secondary screen module can also optimize performance
``` ts
import { loadRemote } from '@module-federation/enhanced/runtime';

loadRemote('app2/Button');
```

### 9. What to do if you want to use Vue or other frameworks?
We provide a general Web API, but we do not provide a hook like `usePrefetch` in other frameworks such as Vue. We will support it later.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 0 additions & 5 deletions apps/website-new/docs/zh/guide/_meta.json
Original file line number Diff line number Diff line change
@@ -13,10 +13,5 @@
"type": "dir",
"name": "framework",
"label": "框架"
},
{
"type": "dir",
"name": "performance",
"label": "性能优化"
}
]
5 changes: 5 additions & 0 deletions apps/website-new/docs/zh/practice/_meta.json
Original file line number Diff line number Diff line change
@@ -19,5 +19,10 @@
"type": "file",
"name": "scenario",
"label": "场景化"
},
{
"type": "dir",
"name": "performance",
"label": "性能优化"
}
]
Original file line number Diff line number Diff line change
@@ -5,16 +5,21 @@
:::

## 什么是 Data Prefetch
Data Prefetch 可以将远程模块接口请求提前到 js、css 加载时并行发出,提升首屏速度。
Data Prefetch 可以将远程模块接口请求提前到 `remoteEntry` 加载完成后立即发出,无需等到组件渲染,提升首屏速度。

## 适用场景
项目首屏前置流程较长(例如需要鉴权等操作)或次屏希望数据直出的场景

- 消费者常规加载流程:

Host HTML(消费者 HTML) -> Host main.js(消费者入口 js) -> Host fetch(消费者鉴权等前置动作) -> Provider main.js(生产者入口 js) -> Provider fetch(生产者发送请求)
`Host HTML`(消费者 HTML) -> `Host main.js`(消费者入口 js) -> `Host fetch`(消费者鉴权等前置动作) -> `Provider main.js`(生产者入口 js) -> `Provider fetch`(生产者发送请求)
![](@public/guide/performance/data-prefetch/common.jpg)
- 使用 Prefetch 后的加载流程
![](@public/guide/performance/data-prefetch/prefetch.jpg)
- 在前置流程中提前调用 `loadRemote` 的加载流程(loadRemote 会将有 prefetch 需求的生产者接口请求同步发送)
![](@public/guide/performance/data-prefetch/advanced-prefetch.jpg)

可以看到生产者的请求被提前到跟部分 js 并行了,**目前在最没有前置操作的简单场景中测试首屏可以有几十至上百毫秒的提升,次屏理论上可以完全做到直出**,在前置流程长的情况下可以大大提升模块整体的渲染速度
可以看到生产者的请求被提前到跟部分 js 并行了,**目前对于首屏的优化效果取决于项目加载流程,次屏理论上可以通过提前调用 `loadRemote` 做到直出**,在前置流程长的情况下可以大大提升模块整体的渲染速度

## 使用方法
1.`生产者``消费者`安装 `@module-federation/enhanced`
@@ -93,8 +98,8 @@ const reFetchParams = {
}
export default function Button () {
const [prefetchResult, reFetchUserInfo] = usePrefetch<UserInfo>({
// 对应生产者 ModuleFederationPlugin 中的 (name + expose),例如 `@mf/app2/Button` 用于消费 `Button.prefetch.ts`
id: '@mf/app2/Button',
// 对应生产者 ModuleFederationPlugin 中的 (name + expose),例如 `app2/Button` 用于消费 `Button.prefetch.ts`
id: 'app2/Button',
// 可选参数,使用 defer 后必填
deferId: 'userInfo',
// default 导出默认可以不传 functionId,此处为举例说明,如果非 default 导出则需要填函数名,
@@ -119,7 +124,7 @@ export default function Button () {
)
};
```
3. 生产者的 ModuleFederationPlugin 配置处设置 `dataPrefetch`: true
3. 生产者的 ModuleFederationPlugin 配置处设置 `dataPrefetch: true`
```ts
new ModuleFederationPlugin({
// ...
@@ -146,7 +151,7 @@ localStorage.setItem('FEDERATION_DEBUG', 1)
#### 类型
``` ts
type Options: <T>{
id: string; // 必填,对应生产者 MF 配置中的 (name + expose),例如 `@mf/app2/Button` 用于消费 `Button.prefetch.ts`。
id: string; // 必填,对应生产者 MF 配置中的 (name + expose),例如 `app2/Button` 用于消费 `Button.prefetch.ts`。
functionId?: string; // 可选(默认是 default),用于获取 .prefetch.ts 文件中 export 的函数名称,函数需要以 Prefetch 结尾(不区分大小写)
deferId?: string; // 可选(使用 defer 后必填),使用 defer 后函数返回值为对象(对象中可以有多个 key 对应多个请求),deferId 为对象中的某个 key,用于获取具体请求
cacheStrategy?: () => boolean; // 可选,一般不用手动管理,由框架管理,用于控制是否更新请求结果缓存,目前在组件卸载后或手动执行 reFetch 函数会刷新缓存
@@ -166,8 +171,8 @@ import { Await } from 'react-router-dom';

export const Button = () => {
const [userInfoPrefetch, reFetchUserInfo] = usePrefetch<UserInfo>({
// 对应生产者 MF 配置中的 (name + expose),例如 `@mf/app2/Button` 用于消费 `Button.prefetch.ts`
id: '@mf/app2/Button',
// 对应生产者 MF 配置中的 (name + expose),例如 `app2/Button` 用于消费 `Button.prefetch.ts`
id: 'app2/Button',
// 可选参数,使用 defer 后必填
deferId: 'userInfo'
// default 导出默认可以不传 functionId,此处为举例说明,如果非 default 导出则需要填函数名,
@@ -201,7 +206,7 @@ export const Button = () => {
``` ts
import { loadRemote } from '@module-federation/enhanced/runtime';

loadRemote('@mf/app2/Button');
loadRemote('app2/Button');
```

#### 注意
@@ -246,8 +251,8 @@ import { usePrefetch } from '@module-federation/enhanced/prefetch';
export const Button = () => {
const [state, setState] = useState(defaultVal);
const [userInfoPrefetch, reFetchUserInfo] = usePrefetch<UserInfo>({
// 对应生产者 MF 配置中的 (name + expose),例如 `@mf/app2/Button` 用于消费 `Button.prefetch.ts`
id: '@mf/app2/Button',
// 对应生产者 MF 配置中的 (name + expose),例如 `app2/Button` 用于消费 `Button.prefetch.ts`
id: 'app2/Button',
// 可选参数,使用 defer 后必填
deferId: 'userInfo',
// default 导出默认可以不传 functionId,此处为举例说明,如果非 default 导出则需要填函数名,
@@ -277,7 +282,7 @@ export const Button = () => {
``` ts
import { loadRemote } from '@module-federation/enhanced/runtime';

loadRemote('@mf/app2/Button');
loadRemote('app2/Button');
```

### 9. Vue 或其他框架想用怎么办
43,260 changes: 23,421 additions & 19,839 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

0 comments on commit b4c2573

Please sign in to comment.