206 lines
9 KiB
Markdown
206 lines
9 KiB
Markdown
---
|
|
phase: 26-pwa-performance
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- ui/src/App.tsx
|
|
- ui/vite.config.ts
|
|
autonomous: true
|
|
requirements:
|
|
- PERF-01
|
|
- PERF-05
|
|
must_haves:
|
|
truths:
|
|
- "All page components in App.tsx are loaded via React.lazy"
|
|
- "Route navigation renders a skeleton fallback during chunk load"
|
|
- "Vite build produces separate vendor chunks for heavy libraries"
|
|
- "Main entry chunk is significantly smaller than the pre-split 1.4 MB"
|
|
artifacts:
|
|
- path: "ui/src/App.tsx"
|
|
provides: "Lazy-loaded page routes with Suspense"
|
|
contains: "lazy("
|
|
- path: "ui/vite.config.ts"
|
|
provides: "Manual chunk splitting for vendor libraries"
|
|
contains: "manualChunks"
|
|
key_links:
|
|
- from: "ui/src/App.tsx"
|
|
to: "ui/src/pages/*"
|
|
via: "React.lazy(() => import('./pages/...'))"
|
|
pattern: "lazy\\("
|
|
- from: "ui/vite.config.ts"
|
|
to: "node_modules"
|
|
via: "manualChunks vendor splitting"
|
|
pattern: "manualChunks"
|
|
---
|
|
|
|
<objective>
|
|
Convert all eager page imports in App.tsx to React.lazy with Suspense, and add manual chunk splitting in vite.config.ts to extract heavy vendor libraries into separate bundles.
|
|
|
|
Purpose: Reduces the main bundle from ~1.4 MB to ~200-400 KB, achieving PERF-01 (initial load < 2s broadband, < 5s on 3G) and contributing to PERF-05 (cached load < 1s with smaller chunks to cache).
|
|
Output: Lazy-loaded App.tsx, vendor-chunked vite.config.ts, verified build output.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/26-pwa-performance/26-RESEARCH.md
|
|
@ui/src/App.tsx
|
|
@ui/vite.config.ts
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Convert App.tsx page imports to React.lazy with Suspense</name>
|
|
<files>ui/src/App.tsx</files>
|
|
<read_first>
|
|
- ui/src/App.tsx
|
|
- ui/src/components/ui/skeleton.tsx
|
|
</read_first>
|
|
<action>
|
|
1. Add `lazy, Suspense` to the React import at the top of App.tsx (import from "react").
|
|
|
|
2. Convert ALL page component imports (lines 9-47 in current file) from eager to lazy. Each page import becomes:
|
|
```
|
|
const Dashboard = lazy(() => import("./pages/Dashboard"));
|
|
const Companies = lazy(() => import("./pages/Companies"));
|
|
const Agents = lazy(() => import("./pages/Agents"));
|
|
const AgentDetail = lazy(() => import("./pages/AgentDetail"));
|
|
const Projects = lazy(() => import("./pages/Projects"));
|
|
const ProjectDetail = lazy(() => import("./pages/ProjectDetail"));
|
|
const ProjectWorkspaceDetail = lazy(() => import("./pages/ProjectWorkspaceDetail"));
|
|
const Issues = lazy(() => import("./pages/Issues"));
|
|
const IssueDetail = lazy(() => import("./pages/IssueDetail"));
|
|
const Routines = lazy(() => import("./pages/Routines"));
|
|
const RoutineDetail = lazy(() => import("./pages/RoutineDetail"));
|
|
const ExecutionWorkspaceDetail = lazy(() => import("./pages/ExecutionWorkspaceDetail"));
|
|
const Goals = lazy(() => import("./pages/Goals"));
|
|
const GoalDetail = lazy(() => import("./pages/GoalDetail"));
|
|
const Approvals = lazy(() => import("./pages/Approvals"));
|
|
const ApprovalDetail = lazy(() => import("./pages/ApprovalDetail"));
|
|
const Costs = lazy(() => import("./pages/Costs"));
|
|
const Activity = lazy(() => import("./pages/Activity"));
|
|
const Inbox = lazy(() => import("./pages/Inbox"));
|
|
const CompanySettings = lazy(() => import("./pages/CompanySettings"));
|
|
const SkillBrowser = lazy(() => import("./pages/SkillBrowser"));
|
|
const SkillDetail = lazy(() => import("./pages/SkillDetail"));
|
|
const CompanyExport = lazy(() => import("./pages/CompanyExport"));
|
|
const CompanyImport = lazy(() => import("./pages/CompanyImport"));
|
|
const DesignGuide = lazy(() => import("./pages/DesignGuide"));
|
|
const InstanceGeneralSettings = lazy(() => import("./pages/InstanceGeneralSettings"));
|
|
const InstanceSettings = lazy(() => import("./pages/InstanceSettings"));
|
|
const InstanceExperimentalSettings = lazy(() => import("./pages/InstanceExperimentalSettings"));
|
|
const PluginManager = lazy(() => import("./pages/PluginManager"));
|
|
const PluginSettings = lazy(() => import("./pages/PluginSettings"));
|
|
const PluginPage = lazy(() => import("./pages/PluginPage"));
|
|
const RunTranscriptUxLab = lazy(() => import("./pages/RunTranscriptUxLab"));
|
|
const OrgChart = lazy(() => import("./pages/OrgChart"));
|
|
const NewAgent = lazy(() => import("./pages/NewAgent"));
|
|
const AuthPage = lazy(() => import("./pages/Auth"));
|
|
const BoardClaimPage = lazy(() => import("./pages/BoardClaim"));
|
|
const CliAuthPage = lazy(() => import("./pages/CliAuth"));
|
|
const InviteLandingPage = lazy(() => import("./pages/InviteLanding"));
|
|
const NotFoundPage = lazy(() => import("./pages/NotFound"));
|
|
```
|
|
|
|
3. Keep ALL non-page imports as eager (Layout, OnboardingWizard, authApi, healthApi, queryKeys, context hooks, lib utils). These are needed for the app shell and should not be lazy.
|
|
|
|
4. Ensure each page module uses `export default` — check if pages use named exports. If they use `export function PageName`, the lazy import syntax needs: `lazy(() => import("./pages/PageName").then(m => ({ default: m.PageName })))`. Check the actual export style and adjust accordingly.
|
|
|
|
5. Wrap the `<Routes>` block inside the `App()` function with a `<Suspense>` boundary:
|
|
```tsx
|
|
<Suspense fallback={<div className="flex items-center justify-center h-full"><Skeleton className="h-8 w-48" /></div>}>
|
|
<Routes>
|
|
{/* existing routes unchanged */}
|
|
</Routes>
|
|
</Suspense>
|
|
```
|
|
Import `Skeleton` from `@/components/ui/skeleton`.
|
|
|
|
6. Do NOT lazy-load `OnboardingWizard` — it renders outside Routes and is always needed.
|
|
</action>
|
|
<verify>
|
|
<automated>grep -c "lazy(" ui/src/App.tsx && grep -q "Suspense" ui/src/App.tsx && echo "PASS"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep -c "lazy(" ui/src/App.tsx` returns 39 or more (one per page component)
|
|
- `grep "Suspense" ui/src/App.tsx` shows Suspense wrapper
|
|
- `grep "Skeleton" ui/src/App.tsx` shows skeleton import and usage in fallback
|
|
- No eager `import { Dashboard }` style page imports remain
|
|
- Non-page imports (Layout, OnboardingWizard, authApi, etc.) remain eager
|
|
- `pnpm --filter @paperclipai/ui build` succeeds without errors
|
|
</acceptance_criteria>
|
|
<done>All page imports converted to React.lazy. Suspense boundary wraps Routes with Skeleton fallback. Build succeeds.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add manual vendor chunk splitting to Vite config</name>
|
|
<files>ui/vite.config.ts</files>
|
|
<read_first>
|
|
- ui/vite.config.ts
|
|
- .planning/phases/26-pwa-performance/26-RESEARCH.md
|
|
</read_first>
|
|
<action>
|
|
1. Add `build.rollupOptions.output.manualChunks` to `vite.config.ts`:
|
|
|
|
```typescript
|
|
build: {
|
|
rollupOptions: {
|
|
output: {
|
|
manualChunks: {
|
|
"vendor-react": ["react", "react-dom"],
|
|
"vendor-router": ["react-router-dom"],
|
|
"vendor-query": ["@tanstack/react-query"],
|
|
"vendor-markdown": ["react-markdown", "remark-gfm"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
```
|
|
|
|
2. Add chunks one at a time conceptually — but write them all at once. The key concern per RESEARCH Pitfall 5 is circular dependency errors. Start with the safest boundaries (react, react-dom are always clean).
|
|
|
|
3. Do NOT include `@mdxeditor/editor` in manualChunks — it has complex internal imports that may cause circular dependency errors. Let Vite's default splitting handle it.
|
|
|
|
4. Do NOT include `rehype-highlight` — it was replaced by manual highlight.js usage in Phase 25.
|
|
|
|
5. Run `pnpm --filter @paperclipai/ui build` after writing the config to verify no circular dependency errors.
|
|
|
|
6. If the build fails with circular dependency errors on any specific chunk, remove that chunk from `manualChunks` and retry. Document which chunks were removed and why in the summary.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @paperclipai/ui build 2>&1 | tail -5 && grep -q "manualChunks" ui/vite.config.ts && echo "PASS"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep "manualChunks" ui/vite.config.ts` shows the configuration
|
|
- `grep "vendor-react" ui/vite.config.ts` shows react chunk
|
|
- `pnpm --filter @paperclipai/ui build` succeeds without errors
|
|
- Build output shows multiple vendor chunk files in dist/assets/
|
|
</acceptance_criteria>
|
|
<done>Vite config has manualChunks for vendor splitting. Build succeeds. Multiple vendor chunks produced in dist/assets/.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `pnpm --filter @paperclipai/ui build` succeeds
|
|
- `grep -c "lazy(" ui/src/App.tsx` returns 39+
|
|
- `grep "manualChunks" ui/vite.config.ts` shows configuration
|
|
- Build output in `ui/dist/assets/` shows separate vendor-*.js chunks
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
All page imports lazy-loaded. Vendor chunks extracted. Build succeeds. Main entry bundle significantly reduced from ~1.4 MB baseline.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/26-pwa-performance/26-01-SUMMARY.md`
|
|
</output>
|