From bb32bbb13082836af6a8be1d330aae78c4f5af21 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:01:13 +0800 Subject: [PATCH] perf: SSR-safe lazy loading, loading skeletons, and SourceImage memo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'use client' wrapper components for ClinicalDashboard and DocumentViewer that use next/dynamic with ssr:false — avoids the Server Component restriction - Update page.tsx routes to import the lazy wrappers instead of direct imports - Add route-level loading.tsx skeletons for / and /documents/[id] - Wrap SourceImage in React.memo to avoid unnecessary re-renders - Add optimizePackageImports for lucide-react in next.config.ts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- next.config.ts | 1 + src/app/documents/[id]/loading.tsx | 7 +++++++ src/app/documents/[id]/page.tsx | 2 +- src/app/loading.tsx | 7 +++++++ src/app/page.tsx | 2 +- src/components/ClinicalDashboard.tsx | 5 +++-- src/components/clinical-dashboard-lazy.tsx | 11 +++++++++++ src/components/document-viewer-lazy.tsx | 10 ++++++++++ 8 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 src/app/documents/[id]/loading.tsx create mode 100644 src/app/loading.tsx create mode 100644 src/components/clinical-dashboard-lazy.tsx create mode 100644 src/components/document-viewer-lazy.tsx diff --git a/next.config.ts b/next.config.ts index 1d0afeba7c..517c438532 100644 --- a/next.config.ts +++ b/next.config.ts @@ -42,6 +42,7 @@ const nextConfig: NextConfig = { devIndicators: false, experimental: { cpus: 1, + optimizePackageImports: ["lucide-react"], }, poweredByHeader: false, turbopack: { diff --git a/src/app/documents/[id]/loading.tsx b/src/app/documents/[id]/loading.tsx new file mode 100644 index 0000000000..81ca711676 --- /dev/null +++ b/src/app/documents/[id]/loading.tsx @@ -0,0 +1,7 @@ +export default function Loading() { + return ( +
+
+
+ ); +} diff --git a/src/app/documents/[id]/page.tsx b/src/app/documents/[id]/page.tsx index 0950bc1fb1..17f79250aa 100644 --- a/src/app/documents/[id]/page.tsx +++ b/src/app/documents/[id]/page.tsx @@ -1,4 +1,4 @@ -import { DocumentViewer } from "@/components/DocumentViewer"; +import { DocumentViewerLazy as DocumentViewer } from "@/components/document-viewer-lazy"; export default async function DocumentPage({ params, diff --git a/src/app/loading.tsx b/src/app/loading.tsx new file mode 100644 index 0000000000..81ca711676 --- /dev/null +++ b/src/app/loading.tsx @@ -0,0 +1,7 @@ +export default function Loading() { + return ( +
+
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 40d4f1a758..e6d9b98a0e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,4 @@ -import { ClinicalDashboard } from "@/components/clinical-dashboard"; +import { ClinicalDashboardLazy as ClinicalDashboard } from "@/components/clinical-dashboard-lazy"; import { isAppModeId, isAppModeVisible, type AppModeId } from "@/lib/app-modes"; type HomeProps = { diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 45a17cf9df..e366a8467a 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -53,6 +53,7 @@ import { import { type CSSProperties, FormEvent, + memo, useCallback, useEffect, useMemo, @@ -509,7 +510,7 @@ function subscribeAuthEmail(onStoreChange: () => void) { }; } -function SourceImage({ +const SourceImage = memo(function SourceImage({ endpoint, caption, className = "max-h-52", @@ -609,7 +610,7 @@ function SourceImage({ className={cn(className, "w-full rounded-lg object-contain")} /> ); -} +}); function ScopeAndGovernanceNotice({ scope, diff --git a/src/components/clinical-dashboard-lazy.tsx b/src/components/clinical-dashboard-lazy.tsx new file mode 100644 index 0000000000..70bd7aafc0 --- /dev/null +++ b/src/components/clinical-dashboard-lazy.tsx @@ -0,0 +1,11 @@ +"use client"; + +import dynamic from "next/dynamic"; + +// `ssr: false` requires a Client Component in the App Router; this wrapper +// keeps the heavy dashboard bundle browser-only for the server-rendered +// home page. +export const ClinicalDashboardLazy = dynamic( + () => import("@/components/clinical-dashboard").then((m) => m.ClinicalDashboard), + { ssr: false }, +); diff --git a/src/components/document-viewer-lazy.tsx b/src/components/document-viewer-lazy.tsx new file mode 100644 index 0000000000..f910a6c2b7 --- /dev/null +++ b/src/components/document-viewer-lazy.tsx @@ -0,0 +1,10 @@ +"use client"; + +import dynamic from "next/dynamic"; + +// `ssr: false` requires a Client Component in the App Router; this wrapper +// keeps the viewer bundle browser-only for the server-rendered document page. +export const DocumentViewerLazy = dynamic( + () => import("@/components/DocumentViewer").then((m) => m.DocumentViewer), + { ssr: false }, +);