React Router 6.4 引入了“Data Router”这一概念,专注于将数据获取与渲染分离,以消除渲染+获取导致请求瀑布流问题以及随之而来的旋转加载图标,极大地提升了页面的 LCP 性能指标。
渲染+获取导致请求瀑布流问题
当路由不知道你的数据需求时,会发生请求瀑布流问题,只有子组件被渲染才能发现后续的数据需求:
使用 React Router 6.4 “Data Router”允许你并行化获取所有数据:
为了实现这一点,“Data Router”将你的路由定义移到渲染周期外,以便可以提前识别嵌套的数据需求。
// app.jsx
import Layout, { getUser } from `./layout`;
import Home from `./home`;
import Projects, { getProjects } from `./projects`;
import Project, { getProject } from `./project`;
const routes = [{
path: '/',
loader: () => getUser(),
element: <Layout />,
children: [{
index: true,
element: <Home />,
}, {
path: 'projects',
loader: () => getProjects(),
element: <Projects />,
children: [{
path: ':projectId',
loader: ({ params }) => getProject(params.projectId),
element: <Project />,
}],
}],
}]
但是这也有缺点。到目前为止,我们讨论了如何优化数据获取,但是我们也必须考虑如何优化 Javascript 包获取!通过上面的路由定义,虽然我们可以并行化请求所有数据,但会下载包含所有 loader 和组件的 Javascript 包会阻塞数据获取的开始。
考虑一个用户通过 /
路径访问你的站点:
用户仍然需要下载 projects
和 :projectId
路由的 loader 和组件,即使用户不需要它们!在最坏的情况下,用户不访问这些路由,他们将永远不会用到它们。这对我们的用户体验来说不是理想的。
使用 React.lazy
React.lazy 提供了一个一流的原语来拆分组件树,但是它产生类似的渲染+获取导致的请求瀑布流问题。这是因为当你使用 React.lazy()
时,你为你的组件创建了一个异步 chunk,但是 React 直到它渲染该组件时才会开始获取该 chunk。
// app.jsx
const LazyComponent = React.lazy(() => import("./component"));
function App() {
return (
<React.Suspense fallback={<p>Loading lazy chunk...</p>}>
<LazyComponent />
</React.Suspense>
);
}
使用 Route.lazy
如果希望 React.lazy 能够与“Data Router”配合使用,我们需要在渲染周期之外引入。就像我们从渲染周期中提取数据获取一样,我们也希望从渲染周期中提取 chunk 获取。
“Data Router” 的路由的属性可以分为3类:
- 路径匹配属性,例如
path
、index
和children
- 数组加载或提交属性,例如
loader
和action
- 渲染属性,例如
element
和errorElement
下面是使用路由新的属性 lazy()
方法的示例:
// app.jsx
import Layout, { getUser } from `./layout`;
import Home from `./home`;
const routes = [{
path: '/',
loader: () => getUser(),
element: <Layout />,
children: [{
index: true,
element: <Home />,
}, {
path: 'projects',
lazy: () => import("./projects"), // ? 惰性加载!
children: [{
path: ':projectId',
lazy: () => import("./project"), // ? 惰性加载!
}],
}],
}]
// projects.jsx
export function loader = () => { ... };
export function Component() { ... }
// project.jsx
export function loader = () => { ... };
export function Component() { ... }
在初始加载时生成的网络图看起来像这样:
现在我们的路由只包括那些我们认为对我们网站的初始入口必要的路由模块。然后,当用户单击指向 /projects/123
的链接时,我们通过 lazy()
方法并行获取这些路由并执行它们返回的 loader
方法:
高级使用和优化
这是最佳网络图吗?事实证明,不是!
在上面的这个例子中,我们的路由模块包括我们的 loader
和组件,这意味着我们在使用 loader
获取数据之前下载两者的内容。实际上, loader
通常非常小,触及大多数业务逻辑所在的外部 API。而组件定义了整个用户交互界面,包括随之而来的所有用户交互活动——而且它们可能会变得相当大。
下载大型组件树的 JS 阻塞 loader
(可能会对某些 API 进行 fetch()
)似乎并不明智?如果我们可以把这个?变成这个?呢?
好消息是,通过很少的代码更改就可以实现!如果 loader
或 action
是在路由上静态定义的,那么它将与 lazy()
并行执行。这允许我们通过将 loader 和组件分离到单独的文件中来将 loader 数据获取与组件下载分离:
const routes = [
{
path: "projects",
async loader({ request, params }) {
let { loader } = await import("./projects-loader");
return loader({ request, params });
},
lazy: () => import("./projects-component"),
},
];
也许你有一个 API,它知道如何根据请求 URL 获取给定路由的数据。你可以内联所有 loader,并在数据获取和组件(或路由模块)下载之间实现完全并行化。
const routes = [
{
path: "projects",
loader: ({ request }) => fetchDataForUrl(request.url),
lazy: () => import("./projects-component"),
},
];