Capítulo 34 de 57

Chapter 34: Code Splitting

Core Idea

TanStack Router splits each route into critical configuration (path parsing, search validation, loaders, context) that must be available immediately, and non-critical configuration (component, errorComponent, pendingComponent, notFoundComponent) that can be loaded on demand, either automatically or via the .lazy.tsx file suffix.

Key Concepts

  • Critical route configuration: path parsing/serialization, search param validation, loaders, beforeLoad, route context, static data, links, scripts, styles. Always bundled with the main chunk.
  • Non-critical route configuration: component, errorComponent, pendingComponent, notFoundComponent. Eligible for lazy loading.
  • Automatic code-splitting: enabled by setting autoCodeSplitting: true in the tanstackRouter Vite plugin options. Only works with file-based routing and a supported bundler, not with the CLI alone.
  • .lazy.tsx suffix: manual code-splitting approach; move non-critical exports into a sibling file named <route>.lazy.tsx and use createLazyFileRoute instead of createFileRoute.
  • Virtual routes: if a route file ends up empty after extracting everything into a .lazy.tsx file, delete the original file entirely; TanStack Router generates a virtual route anchor in the route tree automatically.
  • Directory encapsulation: a route file like posts.tsx can be moved into posts/route.tsx to group a route's files (route, lazy, loader) into one directory.
  • Route.lazy() / createLazyRoute: code-based routing equivalent of .lazy.tsx, splitting a route's component into a separate imported file.
  • Data Loader Splitting via lazyFn: loaders can also be split out using lazyFn(() => import('./loader'), 'loader'), though this is discouraged because it adds an extra async round-trip before data fetching starts.
  • getRouteApi: helper to access a route's type-safe hooks (useLoaderData, useLoaderDeps, useMatch, useParams, useRouteContext, useSearch) from a separate file without importing the Route object directly, avoiding circular dependencies.

Code Examples

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({ autoCodeSplitting: true }),
    react(), // must come after the TanStack Router plugin
  ],
})
  • What it demonstrates: enabling automatic code splitting, the recommended approach for file-based routing setups.
// src/routes/posts.tsx (critical)
export const Route = createFileRoute('/posts')({
  loader: fetchPosts,
})

// src/routes/posts.lazy.tsx (non-critical)
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts')({
  component: Posts,
})
function Posts() { /* ... */ }
  • What it demonstrates: manual splitting with .lazy.tsx, separating loader (critical) from component (non-critical).

Key Takeaways

  1. Prefer autoCodeSplitting: true over manual .lazy.tsx files when using a supported bundler; it requires no restructuring.
  2. Never split the loader unless you have a strong reason: it's already an async boundary and is critical for preload-on-hover performance.
  3. The root route (__root.tsx) cannot be code-split since it always renders.
  4. Use getRouteApi instead of importing the Route object in split component files to avoid circular imports.

Connects To

  • Ch 35: Automatic Code Splitting goes deeper into the customization options (codeSplitGroupings, splitBehavior, defaultBehavior) for this same feature.
  • Ch 40: Preloading relies on the loader being immediately available (not split) for fast hover-based preloads.