自动代码分割

自动代码分割

当使用 React Router 的框架功能时,您的应用程序会自动进行代码分割,以提高用户访问您的应用程序时的初始加载性能。

按路由代码分割

考虑以下简单的路由配置

import {
  type RouteConfig,
  route,
} from "@react-router/dev/routes";

export default [
  route("/contact", "./contact.tsx"),
  route("/about", "./about.tsx"),
] satisfies RouteConfig;

不是将所有路由捆绑到一个巨大的构建中,而是引用的模块 (contact.tsxabout.tsx) 成为捆绑器的入口点。

由于这些入口点与 URL 片段耦合,React Router 仅从 URL 即可知道浏览器中需要哪些捆绑包,更重要的是,哪些是不需要的。

如果用户访问 "/about",则将加载 about.tsx 的捆绑包,但不会加载 contact.tsx 的捆绑包。这确保了大大减少初始页面加载的 JavaScript 体积,并加快您的应用程序速度。

移除服务器代码

任何仅服务器端 路由模块 API 都将从捆绑包中移除。考虑以下路由模块

export async function loader() {
  return { message: "hello" };
}

export async function action() {
  console.log(Date.now());
  return { ok: true };
}

export async function headers() {
  return { "Cache-Control": "max-age=300" };
}

export default function Component({ loaderData }) {
  return <div>{loaderData.message}</div>;
}

为浏览器构建后,只有 Component 仍将保留在捆绑包中,因此您可以在其他模块导出中使用仅服务器端代码。

分割路由模块

仅当设置 unstable_splitRouteModules 未来特性标志时,此功能才启用

export default {
  future: {
    unstable_splitRouteModules: true,
  },
};

路由模块 API 的便利性之一是,路由所需的一切都在单个文件中。不幸的是,在某些情况下,当使用 clientLoaderclientActionHydrateFallback API 时,这会带来性能成本。

作为一个基本示例,考虑以下路由模块

import { MassiveComponent } from "~/components";

export async function clientLoader() {
  return await fetch("https://example.com/api").then(
    (response) => response.json()
  );
}

export default function Component({ loaderData }) {
  return <MassiveComponent data={loaderData} />;
}

在此示例中,我们有一个最小的 clientLoader 导出,它进行基本的 fetch 调用,而默认组件导出则大得多。这对性能来说是一个问题,因为这意味着如果我们想在客户端导航到此路由,则必须先下载整个路由模块,然后客户端加载器才能开始运行。

为了将此可视化为时间线

在以下时间线图中,路由模块条中使用不同的字符来表示正在导出的不同路由模块 API。

Get Route Module:  |--=======|
Run clientLoader:            |-----|
Render:                            |-|

相反,我们希望将其优化为以下内容

Get clientLoader:  |--|
Get Component:     |=======|
Run clientLoader:     |-----|
Render:                     |-|

为了实现此优化,React Router 将在生产构建过程中将路由模块拆分为多个较小的模块。在这种情况下,我们将得到两个单独的 虚拟模块 — 一个用于客户端加载器,一个用于组件及其依赖项。

export async function clientLoader() {
  return await fetch("https://example.com/api").then(
    (response) => response.json()
  );
}
import { MassiveComponent } from "~/components";

export default function Component({ loaderData }) {
  return <MassiveComponent data={loaderData} />;
}

此优化在框架模式下自动应用,但您也可以通过 route.lazy 在库模式下实现它,并在我们的博客文章 lazy loading route modules. 中介绍的那样,在多个文件中编写您的路由。

现在这些模块作为单独的模块可用,客户端加载器和组件可以并行下载。这意味着客户端加载器可以在准备就绪后立即执行,而无需等待组件。

当使用更多路由模块 API 时,此优化甚至更加明显。例如,当使用 clientLoaderclientActionHydrateFallback 时,客户端导航期间单个路由模块的时间线可能如下所示

Get Route Module:     |--~~++++=======|
Run clientLoader:                     |-----|
Render:                                     |-|

这将优化为以下内容

Get clientLoader:     |--|
Get clientAction:     |~~|
Get HydrateFallback:  SKIPPED
Get Component:        |=======|
Run clientLoader:        |-----|
Render:                        |-|

请注意,此优化仅在要拆分的路由模块 API 不在同一文件中共享代码时才有效。例如,以下路由模块无法拆分

import { MassiveComponent } from "~/components";

const shared = () => console.log("hello");

export async function clientLoader() {
  shared();
  return await fetch("https://example.com/api").then(
    (response) => response.json()
  );
}

export default function Component({ loaderData }) {
  shared();
  return <MassiveComponent data={loaderData} />;
}

此路由仍将有效,但由于客户端加载器和组件都依赖于同一文件中定义的 shared 函数,因此它将被反优化为单个路由模块。

为了避免这种情况,您可以将导出之间共享的任何代码提取到单独的文件中。例如

export const shared = () => console.log("hello");

然后,您可以将此共享代码导入到您的路由模块中,而不会触发反优化

import { MassiveComponent } from "~/components";
import { shared } from "./shared";

export async function clientLoader() {
  shared();
  return await fetch("https://example.com/api").then(
    (response) => response.json()
  );
}

export default function Component({ loaderData }) {
  shared();
  return <MassiveComponent data={loaderData} />;
}

由于共享代码位于其自己的模块中,因此 React Router 现在能够将此路由模块拆分为两个单独的虚拟模块

import { shared } from "./shared";

export async function clientLoader() {
  shared();
  return await fetch("https://example.com/api").then(
    (response) => response.json()
  );
}
import { MassiveComponent } from "~/components";
import { shared } from "./shared";

export default function Component({ loaderData }) {
  shared();
  return <MassiveComponent data={loaderData} />;
}

如果您的项目对性能特别敏感,您可以将 unstable_splitRouteModules 未来特性标志设置为 "enforce"

export default {
  future: {
    unstable_splitRouteModules: "enforce",
  },
};

如果任何路由模块无法拆分,此设置将引发错误

Error splitting route module: routes/example/route.tsx

- clientLoader

This export could not be split into its own chunk because it shares code with other exports. You should extract any shared code into its own module and then import it within the route module.
文档和示例 CC 4.0