Skip to content

Implement lazy loading for route components#641

Open
mhsnook wants to merge 3 commits into
mainfrom
claude/lazy-load-routes-nzUBc
Open

Implement lazy loading for route components#641
mhsnook wants to merge 3 commits into
mainfrom
claude/lazy-load-routes-nzUBc

Conversation

@mhsnook
Copy link
Copy Markdown
Owner

@mhsnook mhsnook commented May 12, 2026

Summary

This PR refactors the routing architecture to implement lazy loading for route components using TanStack Router's createLazyFileRoute. This improves initial page load performance by code-splitting route components and only loading them when needed.

Key Changes

  • Route Component Splitting: Created .lazy.tsx variants for major route components that contain the actual component logic, while the original .tsx files now only contain route configuration with minimal imports

    • Examples: welcome.lazy.tsx, browse.index.lazy.tsx, learn/$lang.bulk-add.lazy.tsx, learn/$lang.review.index.lazy.tsx, and many others
  • Lazy Route Registration: Updated route files to use createLazyFileRoute instead of createFileRoute, enabling automatic code-splitting and lazy loading of components

  • Route Tree Generation: Updated routeTree.gen.ts to reflect the new lazy-loaded route structure, removing imports for routes that are now lazy-loaded

  • Layout Routes: Converted layout/wrapper routes to lazy loading where appropriate (e.g., _user.lazy.tsx, _auth.lazy.tsx, friends.lazy.tsx)

  • Minimal Route Files: Route configuration files now contain only the route definition and necessary validation schemas, with all UI logic moved to .lazy.tsx files

Implementation Details

  • The pattern follows TanStack Router's lazy loading best practices where route configuration remains in the main file and component implementation is deferred
  • Routes that require authentication or have complex initialization logic maintain their loaders/validators in the main route file
  • The routeTree.gen.ts file was updated to remove imports for lazy-loaded routes, reducing the initial bundle size
  • All lazy route files use createLazyFileRoute with the same route path as their corresponding main route file

https://claude.ai/code/session_01FXH8vJoCT4483fb3sizE6C

Pure-component routes were renamed to *.lazy.tsx using createLazyFileRoute
so only the route stub is in the eager bundle. Routes that also have
config (loader / beforeLoad / validateSearch) were split into foo.tsx
(keeping only the config) and foo.lazy.tsx (component, pendingComponent,
errorComponent). The eager-side import graph is now thinner — no
component-side UI lives in main.

The lazy file's Route is a LazyRoute and doesn't expose .fullPath; the
existing Route.fullPath hints used for type narrowing of absolute Links
were dropped since they were typing aids, not runtime behavior. $lang
Links/Navigates that lost from-inference get explicit params={{ lang }}.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sunlo-tanstack Ready Ready Preview, Comment, Open in v0 May 14, 2026 6:51pm

@supabase
Copy link
Copy Markdown

supabase Bot commented May 12, 2026

This pull request has been ignored for the connected project hepudeougzlgnuqvybrj because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@github-actions
Copy link
Copy Markdown

TypeScript type check

Before: 0 error(s) → After: 0 error(s) (no change)

@github-actions
Copy link
Copy Markdown

Lint (oxlint + eslint)

Before: 11 issue(s) → After: 11 issue(s) (+2 new, −2 resolved)

New (2):

src/routes/_user.lazy.tsx:57:29: 'open', and 'setOpen' [Warning/eslint-plugin-react-hooks(exhaustive-deps)]
src/routes/_user/learn/$lang.review.index.lazy.tsx:87:10: Function `phraseBreakdown` does not capture any variables from its parent scope [Warning/eslint-plugin-unicorn(consistent-function-scoping)]

Resolved (2):

src/routes/_user.tsx:145:29: 'open', and 'setOpen' [Warning/eslint-plugin-react-hooks(exhaustive-deps)]
src/routes/_user/learn/$lang.review.index.tsx:88:10: Function `phraseBreakdown` does not capture any variables from its parent scope [Warning/eslint-plugin-unicorn(consistent-function-scoping)]

@github-actions
Copy link
Copy Markdown

Main bundle size

dist/assets/index-*.js (largest chunk)

Base PR Δ
Raw 1050.25 kB 1048.57 kB 🟢 -1.68 kB (-0.16%)
Gzipped 313.89 kB 312.66 kB 🟢 -1.23 kB (-0.39%)

Writes dist/stats.html on every `pnpm build` so we can see what's
actually in each chunk (used this to diagnose the 1MB main bundle:
react-dom + Supabase createClient + TanStack core are the bulk,
not route components).
@github-actions
Copy link
Copy Markdown

Scenetest

Scenes: 98/98 completed · Assertions: 0/0 passed · Console errors: 0 · Duration: 341734ms

All scenes passed ✅

Extracts each feature's queryKey + queryFn into a small queries.ts file
that doesn't depend on @tanstack/db. Route loaders now call
queryClient.ensureQueryData / prefetchQuery against the same query keys
the collections use. When the lazy collection chunk later loads, its
queryFn finds data already in the React Query cache and skips the fetch.

Also:
  * clear-user.ts and auth-context.tsx use dynamic import() for
    collections — signed-out visitors no longer pay the DB weight.
  * test-runtime-helpers.ts moved its collection imports inside a
    dynamic-import block — the dev/test E2E exposure now only loads
    in DEV builds (the if (import.meta.env.DEV) block tree-shakes
    in prod).
  * SidebarProvider moved from __root.tsx into _user.lazy.tsx. Sidebar
    primitives + @base-ui + @floating-ui + tabbable no longer eager
    for the homepage / auth pages.
  * Toasters in __root.tsx is now React.lazy. lib/utils.ts breaks the
    sonner import chain by dynamic-importing toast functions inside
    copyLink. The two route loaders that called toastNeutral /
    ensureManifestCardsInCollection now dynamic-import on the spot.
  * Drop tailwind-merge; cn() is just clsx (Tailwind v4's cascade
    ordering handles utility conflicts correctly).

Bundle:
  index.js raw:     1,073,940 → 630,343 bytes  (−41.3%)
  index.js gzipped:   321,010 → 185,760 bytes  (−42.1%)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants