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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user