Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split Route Modules #11871

Merged
merged 77 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
0f6a1c7
WIP: Route chunks
markdalgleish Aug 5, 2024
df7d79d
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Aug 20, 2024
5166d95
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Aug 22, 2024
b4c0f70
Migrate playground to routes.ts
markdalgleish Aug 22, 2024
7a4fa89
Separate ctx checks from splitting logic
markdalgleish Aug 22, 2024
f16cada
Add initial `hasChunkableExport` implementation
markdalgleish Aug 23, 2024
e54e8ca
Add initial route chunk generation logic
markdalgleish Aug 26, 2024
3b76b6d
Wire up route chunks logic to Vite plugin
markdalgleish Aug 27, 2024
67b1545
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Aug 28, 2024
e93ae9b
Add initial route chunks HMR support
markdalgleish Aug 28, 2024
5b44c36
Handle new route chunks during HMR
markdalgleish Aug 29, 2024
c46b97a
Fix clientAction chunk query string typo
markdalgleish Sep 2, 2024
287a3bf
Add caching to route chunks logic
markdalgleish Sep 3, 2024
359318e
Improve route chunk generation performance
markdalgleish Sep 4, 2024
c6fb80b
Preload route chunks
markdalgleish Sep 4, 2024
885d5e0
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Sep 13, 2024
b883bf6
Optimize route chunk import statements
markdalgleish Sep 17, 2024
09d9dfd
Leave side effect imports in main route chunk
markdalgleish Sep 17, 2024
1986eab
Handle shared export statements in route chunks
markdalgleish Sep 23, 2024
90399a3
Split exported variable declarations in route chunks
markdalgleish Sep 24, 2024
e4fcc7c
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Sep 24, 2024
825024d
Handle destructured exports in route chunks
markdalgleish Sep 25, 2024
6ade31d
Improve route chunk dependency analysis
markdalgleish Sep 30, 2024
f252822
Add more route chunk unit tests
markdalgleish Sep 30, 2024
61cfbd6
Fix duplicate route chunk code
markdalgleish Oct 1, 2024
6564892
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Oct 2, 2024
cb037e6
Support shared imports in route chunks
markdalgleish Oct 2, 2024
9a2ddcf
Add route chunks integration test
markdalgleish Oct 3, 2024
d8697b2
Skip route chunk HMR preloads when disabled
markdalgleish Oct 4, 2024
aa59841
Refactor to reduce diff
markdalgleish Oct 4, 2024
4131c27
Add route chunks playground
markdalgleish Oct 4, 2024
ee63047
Update lockfile
markdalgleish Oct 6, 2024
c6efa78
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Oct 7, 2024
43ec44b
Fix import removal for omitted route chunks
markdalgleish Oct 8, 2024
8db4099
Increase precedence of route chunk downloads
markdalgleish Oct 8, 2024
2012d2f
Add option to enforce route chunks
markdalgleish Oct 10, 2024
bf71c19
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Nov 4, 2024
e0fb3b4
Fix playground type error
markdalgleish Nov 4, 2024
740f556
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Jan 6, 2025
0696c00
Fix type errors
markdalgleish Jan 6, 2025
1679853
Stop chunking routes in dev
markdalgleish Jan 6, 2025
7e77c73
Fix "Generated an empty chunk" warnings
markdalgleish Jan 7, 2025
472e043
Clean up tests
markdalgleish Jan 7, 2025
728b7e9
Remove unused import
markdalgleish Jan 7, 2025
d44c0b9
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Jan 7, 2025
9eb4c37
Create chunks for HydrateFallback
markdalgleish Jan 8, 2025
c4f3463
Fix HydrateFallback validation in SPA mode
markdalgleish Jan 8, 2025
b6df5bf
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Jan 8, 2025
0ed432e
Remove route-chunks playground entry files
markdalgleish Jan 8, 2025
8afed4c
Run client data and SPA mode tests with route chunks
markdalgleish Jan 8, 2025
2e47509
Add HydrateFallback to bailout check
markdalgleish Jan 9, 2025
d75a338
Refactor
markdalgleish Jan 9, 2025
8b2a756
Skip chunking for root route
markdalgleish Jan 9, 2025
d493476
Handle assets for route chunks
markdalgleish Jan 10, 2025
ca38106
Add HydrateFallback to route chunks test
markdalgleish Jan 14, 2025
d312c78
Add CSS to route chunks test
markdalgleish Jan 14, 2025
b1485f3
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Jan 16, 2025
9df45e1
Skip route chunk build entries when disabled
markdalgleish Jan 16, 2025
30f9f3e
Test mix of chunkable and unchunkable exports
markdalgleish Jan 16, 2025
647d876
Tweak error message
markdalgleish Jan 16, 2025
04a5cda
Rename to "splitRouteModules"
markdalgleish Jan 16, 2025
4da0737
Rename playgrounds
markdalgleish Jan 16, 2025
2fb37e0
Add actions and semi-splittable routes to playgrounds
markdalgleish Jan 16, 2025
0ca355e
Update lockfile
markdalgleish Jan 16, 2025
4077d24
Fix `Route.ComponentProps` usage, update playgrounds
markdalgleish Jan 16, 2025
df6bc68
Migrate tests to Route.ComponentProps
markdalgleish Jan 16, 2025
88f9e6e
Prefetch all route module chunks
markdalgleish Jan 17, 2025
7513e6d
Add docs
markdalgleish Jan 17, 2025
b009094
Add changeset
markdalgleish Jan 17, 2025
b50ca32
Add HydrateFallback to SSR preloads
markdalgleish Jan 20, 2025
9ea41ea
Refactor
markdalgleish Jan 20, 2025
4ac344d
Add shared browser global usage test
markdalgleish Jan 28, 2025
ce925a1
Fix export destructuring detection
markdalgleish Jan 29, 2025
df0f1df
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Jan 29, 2025
3580350
Merge branch 'dev' into markdalgleish/route-chunks
markdalgleish Jan 29, 2025
b0f3af3
Remove redundant checks for missing chunks
markdalgleish Jan 29, 2025
d545369
Expand docs
markdalgleish Jan 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/slimy-suns-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@react-router/dev": patch
"react-router": patch
---

Add unstable support for splitting route modules in framework mode via `future.unstable_splitRouteModules`
192 changes: 191 additions & 1 deletion docs/explanation/code-splitting.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ If the user visits `"/about"` then the bundles for `about.tsx` will be loaded bu

## Removal of Server Code

Any server-only Route Module APIs will be removed from the bundles. Consider this route module:
Any server-only [Route Module APIs][route-module] will be removed from the bundles. Consider this route module:

```tsx
export async function loader() {
Expand All @@ -52,3 +52,193 @@ export default function Component({ loaderData }) {
```

After building for the browser, only the `Component` will still be in the bundle, so you can use server-only code in the other module exports.

## Splitting Route Modules

<docs-info>

This feature is only enabled when setting the `unstable_splitRouteModules` future flag:

```tsx filename=react-router-config.ts
export default {
future: {
unstable_splitRouteModules: true,
},
};
```

</docs-info>

One of the conveniences of the [Route Module API][route-module] is that everything a route needs is in a single file. Unfortunately this comes with a performance cost in some cases when using the `clientLoader`, `clientAction`, and `HydrateFallback` APIs.

As a basic example, consider this route module:

```tsx filename=routes/example.tsx
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} />;
}
```

In this example we have a minimal `clientLoader` export that makes a basic fetch call, whereas the default component export is much larger. This is a problem for performance because it means that if we want to navigate to this route client-side, the entire route module must be downloaded before the client loader can start running.

To visualize this as a timeline:

<docs-info>In the following timeline diagrams, different characters are used within the Route Module bars to denote the different Route Module APIs being exported.</docs-info>

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

Instead, we want to optimize this to the following:

```
Get clientLoader: |--|
Get Component: |=======|
Run clientLoader: |-----|
Render: |-|
```
markdalgleish marked this conversation as resolved.
Show resolved Hide resolved

To achieve this optimization, React Router will split the route module into multiple smaller modules during the production build process. In this case, we'll end up with two separate [virtual modules][virtual-modules] — one for the client loader and one for the component and its dependencies.

```tsx filename=routes/example.tsx?route-chunk=clientLoader
export async function clientLoader() {
return await fetch("https://example.com/api").then(
(response) => response.json()
);
}
```

```tsx filename=routes/example.tsx?route-chunk=main
import { MassiveComponent } from "~/components";

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

<docs-info>This optimization is automatically applied in framework mode, but you can also implement it in library mode via `route.lazy` and authoring your route in multiple files as covered in our blog post on [lazy loading route modules.][blog-lazy-loading-routes]</docs-info>

Now that these are available as separate modules, the client loader and the component can be downloaded in parallel. This means that the client loader can be executed as soon as it's ready without having to wait for the component.

This optimization is even more pronounced when more Route Module APIs are used. For example, when using `clientLoader`, `clientAction` and `HydrateFallback`, the timeline for a single route module during a client-side navigation might look like this:

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

This would instead be optimized to the following:

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

Note that this optimization only works when the Route Module APIs being split don't share code within the same file. For example, the following route module can't be split:

```tsx filename=routes/example.tsx
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} />;
}
```

This route will still work, but since both the client loader and the component depend on the `shared` function defined within the same file, it will be de-optimized into a single route module.

To avoid this, you can extract any code shared between exports into a separate file. For example:

```tsx filename=routes/example/shared.tsx
export const shared = () => console.log("hello");
```

You can then import this shared code in your route module without triggering the de-optimization:

```tsx filename=routes/example/route.tsx
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} />;
}
```

Since the shared code is in its own module, React Router is now able to split this route module into two separate virtual modules:

```tsx filename=routes/example/route.tsx?route-chunk=clientLoader
import { shared } from "./shared";

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

```tsx filename=routes/example/route.tsx?route-chunk=main
import { MassiveComponent } from "~/components";
import { shared } from "./shared";

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

If your project is particularly performance sensitive, you can set the `unstable_splitRouteModules` future flag to `"enforce"`:

```tsx filename=react-router-config.ts
export default {
future: {
unstable_splitRouteModules: "enforce",
},
};
```

This setting will raise an error if any route modules can't be split:

```
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.
```

[route-module]: ../../start/framework/route-module
[virtual-modules]: https://vite.dev/guide/api-plugin#virtual-modules-convention
[blog-lazy-loading-routes]: https://remix.run/blog/lazy-loading-routes#advanced-usage-and-optimizations
Loading