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 },
+);