nexus/.planning/phases/26-pwa-performance/26-01-PLAN.md

9 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
26-pwa-performance 01 execute 1
ui/src/App.tsx
ui/vite.config.ts
true
PERF-01
PERF-05
truths artifacts key_links
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
path provides contains
ui/src/App.tsx Lazy-loaded page routes with Suspense lazy(
path provides contains
ui/vite.config.ts Manual chunk splitting for vendor libraries manualChunks
from to via pattern
ui/src/App.tsx ui/src/pages/* React.lazy(() => import('./pages/...')) lazy(
from to via pattern
ui/vite.config.ts node_modules manualChunks vendor splitting manualChunks
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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 Task 1: Convert App.tsx page imports to React.lazy with Suspense ui/src/App.tsx - ui/src/App.tsx - ui/src/components/ui/skeleton.tsx 1. Add `lazy, Suspense` to the React import at the top of App.tsx (import from "react").
  1. 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"));
    
  2. 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.

  3. 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.

  4. Wrap the <Routes> block inside the App() function with a <Suspense> boundary:

    <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.

  5. Do NOT lazy-load OnboardingWizard — it renders outside Routes and is always needed. grep -c "lazy(" ui/src/App.tsx && grep -q "Suspense" ui/src/App.tsx && echo "PASS" <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> All page imports converted to React.lazy. Suspense boundary wraps Routes with Skeleton fallback. Build succeeds.
Task 2: Add manual vendor chunk splitting to Vite config ui/vite.config.ts - ui/vite.config.ts - .planning/phases/26-pwa-performance/26-RESEARCH.md 1. Add `build.rollupOptions.output.manualChunks` to `vite.config.ts`:
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"],
      },
    },
  },
},
  1. 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).

  2. 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.

  3. Do NOT include rehype-highlight — it was replaced by manual highlight.js usage in Phase 25.

  4. Run pnpm --filter @paperclipai/ui build after writing the config to verify no circular dependency errors.

  5. 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. pnpm --filter @paperclipai/ui build 2>&1 | tail -5 && grep -q "manualChunks" ui/vite.config.ts && echo "PASS" <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> Vite config has manualChunks for vendor splitting. Build succeeds. Multiple vendor chunks produced in dist/assets/.
- `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

<success_criteria> All page imports lazy-loaded. Vendor chunks extracted. Build succeeds. Main entry bundle significantly reduced from ~1.4 MB baseline. </success_criteria>

After completion, create `.planning/phases/26-pwa-performance/26-01-SUMMARY.md`