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

feat(router-plugin): configurable key-based code splitting #3355

Merged
merged 40 commits into from
Feb 9, 2025

Conversation

SeanCassiere
Copy link
Member

@SeanCassiere SeanCassiere commented Feb 7, 2025

Customizable grouping of the code-splittable parts.

The goal of this change is as follows.

  • To give the user, control over how the code-splittable parts (at the moment, the loader and component) are split into their own virtual route bundles.
    • Currently, when a route file is encountered, it enters two diverging paths where code transformation occurs.
    • You have the "reference" route (AKA the original route), which is transformed to import the loader and component from a "virtual" route.
    • This "virtual" route is generated by taking the content of the "reference" route and then performing code transformations to only have the loader and component being exported out of it by the end.
    • This means, that currently both the loader and component are being exported out of the same "virtual" file. This change makes it so that the user can control how this behaviour works.
  • To support splitting out new parts of the "reference" route. Namely the addition comprises of now supporting pendingComponent, notFoundComponent, and the errorComponent.
    • Therefore, the list of items that can be automatically code split out will be loader, component, pendingComponent, errorComponent, and notFoundComponent.
  • To change the default grouping of [['loader', 'component']] to this new grouping:
    • [ ['component'], ['pendingComponent'], ['notFoundComponent'], ['errorComponent'] ]
    • In this new default grouping, the loader will NOT be extracted out to follow the guidelines set in our documentation.
      ## How does TanStack Router split code?
      TanStack Router separates code into two categories:
      - **Critical Route Configuration** - The code that is required to render the current route and kick off the data loading process as early as possible.
      - Path Parsing/Serialization
      - Search Param Validation
      - Loaders, Before Load
      - Route Context
      - Static Data
      - Links
      - Scripts
      - Styles
      - All other route configuration not listed below
      - **Non-Critical/Lazy Route Configuration** - The code that is not required to match the route, and can be loaded on-demand.
      - Route Component
      - Error Component
      - Pending Component
      - Not-found Component
      > 🧠 **Why is the loader not split?**
      >
      > - The loader is already an asynchronous boundary, so you pay double to both get the chunk _and_ wait for the loader to execute.
      > - Categorically, it is less likely to contribute to a large bundle size than a component.
      > - The loader is one of the most important preloadable assets for a route, especially if you're using a default preload intent, like hovering over a link, so it's important for the loader to be available without any additional async overhead.
      >
      > Knowing the disadvantages of splitting the loader, if you still want to go ahead with it, head over to the [Data Loader Splitting](#data-loader-splitting) section.
      .

What is a grouping?

As mentioned earlier, a grouping will consistent only of the relevant exports as stated by the user.

So, for this reference route.

// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
import { postsLoader, PostsComponent, PostsPendingComponent } from '@modules/posts'

export const Route = createFileRoute('/posts')({
	loader: loader,
	component: PostsComponent,
	pendingComponent: PostsPendingComponent,
	codeSplitGroupings: [
		['component', 'loader'],
		['pendingComponent'],
	]
})

This will be the virtual route generated for ['component', 'loader'].

// src/routes/posts.tsx?tsr-split=component----loader
import { postsLoader, PostsComponent } from '@modules/posts'
const SplitLoader = postsLoader
const SplitComponent = PostsComponent
export { SplitComponent as component, SplitLoader as loader }

And this will be the virtual route generated for ['pendingComponent'].

// src/routes/posts.tsx?tsr-split=pendingComponent
import { PostsPendingComponent } from '@modules/posts'
const SplitPendingComponent = PostsPendingComponent
export { SplitPendingComponent as pendingComponent }

So finally, the transformed "reference" route will look similar to this.

import { lazyFn } from '@tanstack/react-router';
import { createFileRoute } from '@tanstack/react-router';
import { lazyRouteComponent } from '@tanstack/react-router';

const $$splitComponentImporter = () => import('src/routes/posts.tsx?tsr-split=component----loader');
const $$splitLoaderImporter = () => import('src/routes/posts.tsx?tsr-split=component----loader');
const $$splitPendingComponentImporter = () => import('src/routes/posts.tsx?tsr-split=pendingComponent');

export const Route = createFileRoute('/posts')({
  loader: lazyFn($$splitLoaderImporter, 'loader'),
  component: lazyRouteComponent($$splitComponentImporter, 'component', () => Route.ssr),
  pendingComponent: lazyRouteComponent($$splitPendingComponentImporter, 'pendingComponent')
});

How does this work?

This flow is the flow for how the "reference" and "virtual" routes are formed.

image

How is this configured?

As shown in the diagram, you have three layers at which configuration can occur:

1. Route-level configuration

If the codeSplitGroupings value is set, the router-plugin will disregard all other settings further down the tree.

// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
import { postsLoader, PostsComponent, PostsPendingComponent } from '@modules/posts'

export const Route = createFileRoute('/posts')({
	loader: loader,
	component: PostsComponent,
	pendingComponent: PostsPendingComponent,
	codeSplitGroupings: [
		['loader'],
		['component', 'pendingComponent']
	]
})

2. Plugin function-based configuration API

This is a function that lets you programmatically change the code-splitting behaviour.

// vite.config.ts
import { defineConfig } from 'vite'
import { TanStackRouterVite } from '@tanstack/router/vite'

export default defineConfig({
	plugins: [
		TanStackRouterVite({
			codeSplittingOptions: {
				splitBehaviour: ({ routeId }) => {
					if (routeId.startsWith('/posts')) {
						return [
							['loader', 'component'],
							['errorComponent', 'notFoundComponent', 'pendingComponent']
						]
					}
				}
			}
		})
	]
})

3. Plugin global configuration API

This is the last stop on the train checking for user-controlled code splitting.

// vite.config.ts
import { defineConfig } from 'vite'
import { TanStackRouterVite } from '@tanstack/router/vite'

export default defineConfig({
	plugins: [
		TanStackRouterVite({
			codeSplittingOptions: {
				defaultGroupings: [
					['loader'],
					['component'],
					['pendingComponent'],
					['notFoundComponent', 'errorComponent']
				]
			}
		})
	]
})

Closes #2524

Copy link

nx-cloud bot commented Feb 7, 2025

View your CI Pipeline Execution ↗ for commit ea18741.

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 3m 26s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 26s View ↗

☁️ Nx Cloud last updated this comment at 2025-02-09 00:49:57 UTC

Copy link

pkg-pr-new bot commented Feb 7, 2025

Open in Stackblitz

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@3355

@tanstack/create-router

npm i https://pkg.pr.new/@tanstack/create-router@3355

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/@tanstack/directive-functions-plugin@3355

@tanstack/create-start

npm i https://pkg.pr.new/@tanstack/create-start@3355

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@3355

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@3355

@tanstack/react-cross-context

npm i https://pkg.pr.new/@tanstack/react-cross-context@3355

@tanstack/react-router-with-query

npm i https://pkg.pr.new/@tanstack/react-router-with-query@3355

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@3355

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@3355

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@3355

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@3355

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@3355

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@3355

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@3355

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@3355

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/@tanstack/server-functions-plugin@3355

@tanstack/start

npm i https://pkg.pr.new/@tanstack/start@3355

@tanstack/start-api-routes

npm i https://pkg.pr.new/@tanstack/start-api-routes@3355

@tanstack/start-client

npm i https://pkg.pr.new/@tanstack/start-client@3355

@tanstack/start-config

npm i https://pkg.pr.new/@tanstack/start-config@3355

@tanstack/start-plugin

npm i https://pkg.pr.new/@tanstack/start-plugin@3355

@tanstack/start-router-manifest

npm i https://pkg.pr.new/@tanstack/start-router-manifest@3355

@tanstack/start-server

npm i https://pkg.pr.new/@tanstack/start-server@3355

@tanstack/start-server-functions-client

npm i https://pkg.pr.new/@tanstack/start-server-functions-client@3355

@tanstack/start-server-functions-fetcher

npm i https://pkg.pr.new/@tanstack/start-server-functions-fetcher@3355

@tanstack/start-server-functions-handler

npm i https://pkg.pr.new/@tanstack/start-server-functions-handler@3355

@tanstack/start-server-functions-server

npm i https://pkg.pr.new/@tanstack/start-server-functions-server@3355

@tanstack/start-server-functions-ssr

npm i https://pkg.pr.new/@tanstack/start-server-functions-ssr@3355

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@3355

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@3355

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@3355

commit: ea18741

@SeanCassiere SeanCassiere changed the title refactor(router-plugin): split component and loader separately refactor(router-plugin): configurable key-based code splitting Feb 8, 2025
@schiller-manuel schiller-manuel force-pushed the refactor/split-loader-and-component-bundles branch from 661052f to 678f65c Compare February 8, 2025 14:10
@schiller-manuel schiller-manuel changed the title refactor(router-plugin): configurable key-based code splitting feat(router-plugin): configurable key-based code splitting Feb 8, 2025
@SeanCassiere SeanCassiere marked this pull request as ready for review February 8, 2025 23:45
@SeanCassiere SeanCassiere merged commit cf693e7 into main Feb 9, 2025
5 checks passed
@SeanCassiere SeanCassiere deleted the refactor/split-loader-and-component-bundles branch February 9, 2025 01:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Not found, pending, and error components are not automatically code split
2 participants