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
+86 -79
View File
@@ -6,7 +6,7 @@ import Link from "next/link";
import type { Route } from "next";
import { usePathname } from "next/navigation";
import { clsx } from "clsx";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { PreferencesModal } from "./PreferencesModal.js";
import { ThemeProvider } from "./ThemeProvider.js";
@@ -231,6 +231,60 @@ function NavTooltip({
);
}
/* ------------------------------------------------------------------ */
/* Memoized nav item — prevents re-render of inactive items */
/* ------------------------------------------------------------------ */
/** Routes that benefit from eager prefetching (loaded while user reads current page). */
const PREFETCH_ROUTES = new Set(["/dashboard", "/timeline", "/projects", "/resources", "/allocations"]);
const NavItemLink = memo(function NavItemLink({
href,
label,
icon,
isActive,
collapsed,
onClick,
}: {
href: string;
label: string;
icon: ReactNode;
isActive: boolean;
collapsed: boolean;
onClick?: (() => void) | undefined;
}) {
const linkProps = {
...(onClick ? { onClick } : {}),
...(PREFETCH_ROUTES.has(href) ? { prefetch: true as const } : {}),
};
return (
<NavTooltip label={label} show={collapsed}>
<Link
href={href as Route}
{...linkProps}
className={clsx(
"group relative flex items-center rounded-2xl text-sm font-medium transition-colors",
collapsed ? "justify-center px-2 py-2" : "gap-3 px-3 py-2",
isActive
? "text-brand-800 dark:text-brand-200"
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
)}
>
{isActive && (
<motion.div
layoutId="nav-indicator"
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-brand-100 to-brand-50/80 shadow-sm ring-1 ring-brand-200/80 dark:from-brand-900/30 dark:to-brand-800/20 dark:ring-brand-900/40"
transition={{ type: "spring", stiffness: 350, damping: 30 }}
/>
)}
<IconFrame>{icon}</IconFrame>
{!collapsed && <span className="relative flex-1">{label}</span>}
</Link>
</NavTooltip>
);
});
/* ------------------------------------------------------------------ */
/* Sidebar component */
/* ------------------------------------------------------------------ */
@@ -370,34 +424,17 @@ function SidebarContent({
className="overflow-hidden"
>
<div className="space-y-0.5">
{section.items.map((item) => {
const isActive = activeHrefSet.has(item.href);
return (
<NavTooltip key={item.href} label={item.label} show={sidebarCollapsed}>
<Link
href={item.href as Route}
onClick={handleLinkClick}
className={clsx(
"group relative flex items-center rounded-2xl text-sm font-medium transition-colors",
sidebarCollapsed ? "justify-center px-2 py-2" : "gap-3 px-3 py-2",
isActive
? "text-brand-800 dark:text-brand-200"
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
)}
>
{isActive && (
<motion.div
layoutId="nav-indicator"
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-brand-100 to-brand-50/80 shadow-sm ring-1 ring-brand-200/80 dark:from-brand-900/30 dark:to-brand-800/20 dark:ring-brand-900/40"
transition={{ type: "spring", stiffness: 350, damping: 30 }}
/>
)}
<IconFrame>{item.icon}</IconFrame>
{!sidebarCollapsed && <span className="relative flex-1">{item.label}</span>}
</Link>
</NavTooltip>
);
})}
{section.items.map((item) => (
<NavItemLink
key={item.href}
href={item.href}
label={item.label}
icon={item.icon}
isActive={activeHrefSet.has(item.href)}
collapsed={sidebarCollapsed}
onClick={handleLinkClick}
/>
))}
</div>
</motion.div>
)}
@@ -425,32 +462,17 @@ function SidebarContent({
if (sidebarCollapsed) {
// In collapsed mode, show sub-group items directly as icon-only
return entry.items.map((item) => {
const isActive = activeHrefSet.has(item.href);
return (
<NavTooltip key={item.href} label={item.label} show>
<Link
href={item.href as Route}
onClick={handleLinkClick}
className={clsx(
"group relative flex items-center justify-center rounded-2xl px-2 py-2 text-sm font-medium transition-colors",
isActive
? "text-brand-800 dark:text-brand-200"
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
)}
>
{isActive && (
<motion.div
layoutId="nav-indicator"
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-brand-100 to-brand-50/80 shadow-sm ring-1 ring-brand-200/80 dark:from-brand-900/30 dark:to-brand-800/20 dark:ring-brand-900/40"
transition={{ type: "spring", stiffness: 350, damping: 30 }}
/>
)}
<IconFrame>{item.icon}</IconFrame>
</Link>
</NavTooltip>
);
});
return entry.items.map((item) => (
<NavItemLink
key={item.href}
href={item.href}
label={item.label}
icon={item.icon}
isActive={activeHrefSet.has(item.href)}
collapsed
onClick={handleLinkClick}
/>
));
}
return (
@@ -517,31 +539,16 @@ function SidebarContent({
);
}
const isActive = activeHrefSet.has(entry.href);
return (
<NavTooltip key={entry.href} label={entry.label} show={sidebarCollapsed}>
<Link
href={entry.href as Route}
onClick={handleLinkClick}
className={clsx(
"group relative flex items-center rounded-2xl text-sm font-medium transition-colors",
sidebarCollapsed ? "justify-center px-2 py-2" : "gap-3 px-3 py-2",
isActive
? "text-brand-800 dark:text-brand-200"
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
)}
>
{isActive && (
<motion.div
layoutId="nav-indicator"
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-brand-100 to-brand-50/80 shadow-sm ring-1 ring-brand-200/80 dark:from-brand-900/30 dark:to-brand-800/20 dark:ring-brand-900/40"
transition={{ type: "spring", stiffness: 350, damping: 30 }}
/>
)}
<IconFrame>{entry.icon}</IconFrame>
{!sidebarCollapsed && <span className="relative flex-1">{entry.label}</span>}
</Link>
</NavTooltip>
<NavItemLink
key={entry.href}
href={entry.href}
label={entry.label}
icon={entry.icon}
isActive={activeHrefSet.has(entry.href)}
collapsed={sidebarCollapsed}
onClick={handleLinkClick}
/>
);
})}
</div>