Route-level code splitting for faster mobile first paint (#106) #113
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { Suspense, useEffect, useState } from 'react'
|
||||||
import { Link, Outlet, useMatchRoute, useNavigate, useRouterState } from '@tanstack/react-router'
|
import { Link, Outlet, useMatchRoute, useNavigate, useRouterState } from '@tanstack/react-router'
|
||||||
import { Toaster } from '@/components/ui/toast'
|
import { Toaster } from '@/components/ui/toast'
|
||||||
import { useLogout, useMe } from '@/lib/auth'
|
import { useLogout, useMe } from '@/lib/auth'
|
||||||
@@ -93,7 +93,10 @@ export function AppShell() {
|
|||||||
showBottomNav && 'pb-[calc(3.5rem+env(safe-area-inset-bottom))] md:pb-6',
|
showBottomNav && 'pb-[calc(3.5rem+env(safe-area-inset-bottom))] md:pb-6',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/* Boundary for the lazily-loaded routes (see router.tsx). */}
|
||||||
|
<Suspense fallback={<p className="p-6 text-sm text-muted">Loading…</p>}>
|
||||||
|
|
|||||||
<Outlet />
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{showBottomNav && <BottomNav sections={visibleSections} />}
|
{showBottomNav && <BottomNav sections={visibleSections} />}
|
||||||
|
|||||||
+20
-5
@@ -1,3 +1,4 @@
|
|||||||
|
import { lazy } from 'react'
|
||||||
import {
|
import {
|
||||||
createRootRouteWithContext,
|
createRootRouteWithContext,
|
||||||
createRoute,
|
createRoute,
|
||||||
@@ -9,13 +10,27 @@ import { AppShell } from '@/components/layout/AppShell'
|
|||||||
import { NotFound } from '@/components/NotFound'
|
import { NotFound } from '@/components/NotFound'
|
||||||
import { RouteError } from '@/components/RouteError'
|
import { RouteError } from '@/components/RouteError'
|
||||||
import { LoginPage } from '@/pages/LoginPage'
|
import { LoginPage } from '@/pages/LoginPage'
|
||||||
import { RegisterPage } from '@/pages/RegisterPage'
|
|
||||||
import { GardensPage } from '@/pages/GardensPage'
|
import { GardensPage } from '@/pages/GardensPage'
|
||||||
import { GardenEditorPage } from '@/pages/GardenEditorPage'
|
|
||||||
import { PublicGardenPage } from '@/pages/PublicGardenPage'
|
|
||||||
import { PlantsPage } from '@/pages/PlantsPage'
|
|
||||||
import { SettingsPage } from '@/pages/SettingsPage'
|
|
||||||
import { meQueryOptions } from '@/lib/auth'
|
import { meQueryOptions } from '@/lib/auth'
|
||||||
|
|
||||||
|
// Code-split the heavier / deeper routes so a phone on cell data doesn't download
|
||||||
|
// the whole app (notably the canvas editor with its gesture + geometry deps)
|
||||||
|
// before the first screen paints. Login and the gardens list — the entry points —
|
||||||
|
// stay eager to avoid a fallback flash on landing; AppShell wraps the Outlet in a
|
||||||
|
// Suspense boundary for the rest.
|
||||||
|
const GardenEditorPage = lazy(() =>
|
||||||
|
import('@/pages/GardenEditorPage').then((m) => ({ default: m.GardenEditorPage })),
|
||||||
|
)
|
||||||
|
const PublicGardenPage = lazy(() =>
|
||||||
|
import('@/pages/PublicGardenPage').then((m) => ({ default: m.PublicGardenPage })),
|
||||||
|
)
|
||||||
|
const PlantsPage = lazy(() => import('@/pages/PlantsPage').then((m) => ({ default: m.PlantsPage })))
|
||||||
|
const SettingsPage = lazy(() =>
|
||||||
|
import('@/pages/SettingsPage').then((m) => ({ default: m.SettingsPage })),
|
||||||
|
)
|
||||||
|
const RegisterPage = lazy(() =>
|
||||||
|
import('@/pages/RegisterPage').then((m) => ({ default: m.RegisterPage })),
|
||||||
|
)
|
||||||
import { queryClient } from '@/lib/queryClient'
|
import { queryClient } from '@/lib/queryClient'
|
||||||
import { safeRedirectPath } from '@/lib/redirect'
|
import { safeRedirectPath } from '@/lib/redirect'
|
||||||
import { getLastGardenId } from '@/lib/lastGarden'
|
import { getLastGardenId } from '@/lib/lastGarden'
|
||||||
|
|||||||
@@ -29,6 +29,20 @@ export default defineConfig(({ mode }) => {
|
|||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// One vendor chunk for ALL node_modules: it's rarely-changing, so it
|
||||||
|
// caches across app deploys while the tiny app chunk churns. Kept as a
|
||||||
|
// SINGLE chunk on purpose — splitting react-dom/scheduler into their own
|
||||||
|
// chunk reorders their module init across chunk boundaries and breaks
|
||||||
|
// React 19 at load ("Cannot set 'Activity' of undefined"). The heavy
|
||||||
|
// routes are code-split separately via React.lazy (router.tsx), which is
|
||||||
|
// where the real first-paint win is.
|
||||||
|
manualChunks(id) {
|
||||||
|
gitea-actions
commented
🟠 manualChunks forces editor-only deps (@use-gesture, geometry) into the eager vendor chunk, defeating the first-paint split for those libraries performance · flagged by 1 model
🪰 Gadfly · advisory 🟠 **manualChunks forces editor-only deps (@use-gesture, geometry) into the eager vendor chunk, defeating the first-paint split for those libraries**
_performance · flagged by 1 model_
- **`web/vite.config.ts:41-43` — the gesture/geometry deps the PR wants off the first-paint path are actually pulled *into* it.** The `manualChunks(id)` function unconditionally returns `'vendor'` for **every** `node_modules` module, and that vendor chunk is on the critical path for *every* route (including the eager `/login` and `/gardens`). I verified that `@use-gesture/react` (the dep the PR description names as the biggest surface to keep lazy) is only referenced from `web/src/editor/useView…
<sub>🪰 Gadfly · advisory</sub>
|
|||||||
|
return id.includes('node_modules') ? 'vendor' : undefined
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user
🟠 Lazy-route chunk load failures are unrecoverable: the error boundary's retry (router.invalidate()) doesn't reload the module, so a stale-deploy client is stuck failing forever
error-handling · flagged by 1 model
web/src/components/layout/AppShell.tsx:97(Suspense boundary for the new lazy routes) +web/src/router.tsx:14-33(thelazy()definitions) +web/src/components/RouteError.tsx:14: no handling for a chunk-load failure on a stale client.React.lazy(load)memoizes theimport()promise once per component definition — if it rejects (network blip, or the classic case here: the SPA stays open in a tab across a deploy, and the hashed chunk filename referenced by the already-loaded `index.htm…🪰 Gadfly · advisory