自動程式碼分割

自動程式碼分割

當使用 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 在函式庫模式中實作,並在多個檔案中撰寫您的路由,如我們關於延遲載入路由模組的部落格文章中所述。

現在這些可以作為獨立模組使用,用戶端載入器和元件可以並行下載。這表示用戶端載入器可以在準備就緒後立即執行,而無需等待元件。

當使用更多路由模組 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