The whole app shipped in one 578 KB chunk, so a phone on cell data
downloaded and parsed everything — the canvas editor, gestures, geometry,
every page — before the login screen could paint.
- Lazy-load the heavy/deep routes via React.lazy: the editor (its
GardenCanvas + use-gesture + geometry are the biggest surface), the
public garden view, plants, settings, register. Login and the gardens
list stay eager (entry points — no fallback flash on landing). AppShell
wraps <Outlet> in a Suspense boundary.
- One `vendor` manualChunk for all node_modules so the rarely-changing
libraries cache across app deploys while the tiny app chunk churns.
Kept as a SINGLE chunk deliberately: splitting react-dom/scheduler into
their own chunk reorders module init across chunk boundaries and breaks
React 19 at load ("Cannot set 'Activity' of undefined") — verified that
failure and backed it out.
Result: app entry chunk 578 KB → 39 KB; vendor 421 KB (cached); the editor
(43 KB) + canvas (17 KB) only download when you open a garden. No more
>500 KB chunk warning.
Verified live against the embedded binary: /gardens loads with only
index+vendor; opening a garden lazy-fetches the editor chunk and renders;
console clean; the embed serves the hashed split chunks + SPA fallback fine.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
49 lines
1.7 KiB
TypeScript
49 lines
1.7 KiB
TypeScript
import { defineConfig, loadEnv } from 'vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import { fileURLToPath, URL } from 'node:url'
|
|
|
|
// In dev, Vite serves the SPA with HMR and proxies /api to the Go backend. The
|
|
// backend listens on PANSY_PORT (default 8080); read it so the proxy target
|
|
// tracks an overridden port. The production build is embedded into the Go binary
|
|
// (see internal/webdist), so no proxy is involved there.
|
|
export default defineConfig(({ mode }) => {
|
|
const env = loadEnv(mode, process.cwd(), '')
|
|
const backendPort = env.PANSY_PORT || process.env.PANSY_PORT || '8080'
|
|
return {
|
|
plugins: [react(), tailwindcss()],
|
|
resolve: {
|
|
alias: {
|
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
},
|
|
},
|
|
server: {
|
|
port: 5173,
|
|
proxy: {
|
|
'/api': {
|
|
target: `http://127.0.0.1:${backendPort}`,
|
|
changeOrigin: true,
|
|
},
|
|
},
|
|
},
|
|
build: {
|
|
outDir: 'dist',
|
|
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) {
|
|
return id.includes('node_modules') ? 'vendor' : undefined
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|