perf: lazy-load xlsx/recharts, split estimate tabs, memoize nav

- xlsx dynamically imported via cached singleton in excel.ts and
  skillMatrixParser.ts (removes ~100 kB from 4 routes)
- recharts extracted into lazy-loaded SkillDistributionChart and
  PeakTimesChart components (removes ~60 kB from 3 routes)
- EstimateWorkspaceClient: 7 tab components + 2 editors loaded via
  next/dynamic (reduces /estimates/[id] from 323 kB to 138 kB)
- ImportModal lazy-loaded in ResourcesClient (deferred until open)
- NavItem memoized with React.memo, top 5 routes get prefetch=true
- Raw <img> replaced with next/image in ProjectsClient, CoverArtSection
- tRPC QueryClient: refetchOnWindowFocus/Reconnect disabled globally

Heaviest routes reduced 39-66% First Load JS:
  /analytics/skills: 383→132 kB (-66%)
  /estimates/[id]:   323→138 kB (-57%)
  /resources/[id]:   458→210 kB (-54%)
  /estimates:        310→170 kB (-45%)
  /resources:        363→222 kB (-39%)

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-19 01:23:33 +01:00
parent f1f1be21c7
commit 5ffc0d92e4
15 changed files with 317 additions and 196 deletions
@@ -2,26 +2,70 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import dynamic from "next/dynamic";
import { EstimateExportFormat } from "@planarchy/shared";
import { clsx } from "clsx";
import { EstimateWorkspaceDraftEditor } from "~/components/estimates/EstimateWorkspaceDraftEditor.js";
import { WeeklyPhasingView } from "~/components/estimates/WeeklyPhasingView.js";
import type {
EstimateWorkspaceView,
WorkspaceTab,
} from "~/components/estimates/EstimateWorkspace.types.js";
import { OverviewTab } from "~/components/estimates/tabs/OverviewTab.js";
import { AssumptionsTab } from "~/components/estimates/tabs/AssumptionsTab.js";
import { ScopeTab } from "~/components/estimates/tabs/ScopeTab.js";
import { StaffingTab } from "~/components/estimates/tabs/StaffingTab.js";
import { FinancialsTab } from "~/components/estimates/tabs/FinancialsTab.js";
import { VersionsTab } from "~/components/estimates/tabs/VersionsTab.js";
import { ExportsTab } from "~/components/estimates/tabs/ExportsTab.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { usePermissions } from "~/hooks/usePermissions.js";
import { formatDateLong } from "~/lib/format.js";
import { trpc } from "~/lib/trpc/client.js";
const TabSkeleton = () => (
<div className="p-6 space-y-4">
<div className="h-8 w-48 shimmer-skeleton rounded" />
<div className="h-64 shimmer-skeleton rounded-xl" />
</div>
);
const EstimateWorkspaceDraftEditor = dynamic(
() => import("~/components/estimates/EstimateWorkspaceDraftEditor.js").then((mod) => ({ default: mod.EstimateWorkspaceDraftEditor })),
{ loading: TabSkeleton },
);
const WeeklyPhasingView = dynamic(
() => import("~/components/estimates/WeeklyPhasingView.js").then((mod) => ({ default: mod.WeeklyPhasingView })),
{ loading: TabSkeleton },
);
const OverviewTab = dynamic(
() => import("~/components/estimates/tabs/OverviewTab.js").then((mod) => ({ default: mod.OverviewTab })),
{ loading: TabSkeleton },
);
const AssumptionsTab = dynamic(
() => import("~/components/estimates/tabs/AssumptionsTab.js").then((mod) => ({ default: mod.AssumptionsTab })),
{ loading: TabSkeleton },
);
const ScopeTab = dynamic(
() => import("~/components/estimates/tabs/ScopeTab.js").then((mod) => ({ default: mod.ScopeTab })),
{ loading: TabSkeleton },
);
const StaffingTab = dynamic(
() => import("~/components/estimates/tabs/StaffingTab.js").then((mod) => ({ default: mod.StaffingTab })),
{ loading: TabSkeleton },
);
const FinancialsTab = dynamic(
() => import("~/components/estimates/tabs/FinancialsTab.js").then((mod) => ({ default: mod.FinancialsTab })),
{ loading: TabSkeleton },
);
const VersionsTab = dynamic(
() => import("~/components/estimates/tabs/VersionsTab.js").then((mod) => ({ default: mod.VersionsTab })),
{ loading: TabSkeleton },
);
const ExportsTab = dynamic(
() => import("~/components/estimates/tabs/ExportsTab.js").then((mod) => ({ default: mod.ExportsTab })),
{ loading: TabSkeleton },
);
const TABS: Array<{ id: WorkspaceTab; label: string }> = [
{ id: "overview", label: "Overview" },
{ id: "assumptions", label: "Assumptions" },