From 9a4072129ad3a488bc47b2bc0b2b8e976ea9fd24 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sun, 3 May 2026 11:14:13 +0800 Subject: [PATCH] perf(web): add route-level loading skeletons for instant tab switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'switching is laggy with many accounts' report — root cause is that both /page.tsx and /users/page.tsx are Server Components that block on the API fetch before sending any HTML. During the wait, the previous route stays frozen (no spinner, no feedback) — the user perceives a 'lag' that grows with row count. App Router's loading.tsx convention solves this: Next.js renders it INSTANTLY on navigation, then streams in the real RSC tree once the data fetch resolves. The skeleton matches the shape of the real shell + a few placeholder rows so the swap is layout-stable. Files: - web/components/table-skeleton.tsx — shared skeleton (PageHead + N rows) - web/app/loading.tsx — used for / - web/app/users/loading.tsx — used for /users If row counts keep growing past a few hundred and the table itself becomes the bottleneck (vs the network fetch this addresses), the next step is pagination: accept ?limit=&offset= on /acc/ and /user/ in cm_api.py and add a 'Load more' button (or a virtual list) at the table-component layer. --- web/app/loading.tsx | 5 ++++ web/app/users/loading.tsx | 5 ++++ web/components/table-skeleton.tsx | 50 +++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 web/app/loading.tsx create mode 100644 web/app/users/loading.tsx create mode 100644 web/components/table-skeleton.tsx diff --git a/web/app/loading.tsx b/web/app/loading.tsx new file mode 100644 index 0000000..86e2072 --- /dev/null +++ b/web/app/loading.tsx @@ -0,0 +1,5 @@ +import TableSkeleton from "@/components/table-skeleton"; + +export default function Loading() { + return ; +} diff --git a/web/app/users/loading.tsx b/web/app/users/loading.tsx new file mode 100644 index 0000000..db948f6 --- /dev/null +++ b/web/app/users/loading.tsx @@ -0,0 +1,5 @@ +import TableSkeleton from "@/components/table-skeleton"; + +export default function Loading() { + return ; +} diff --git a/web/components/table-skeleton.tsx b/web/components/table-skeleton.tsx new file mode 100644 index 0000000..810916f --- /dev/null +++ b/web/components/table-skeleton.tsx @@ -0,0 +1,50 @@ +type Props = { eyebrow: string; title: string; rows?: number }; + +/** + * Lightweight skeleton that mimics the AccountsTable / UsersTable shell. + * Rendered by app/loading.tsx and app/users/loading.tsx — Next.js shows + * this immediately on tab navigation, then streams in the real Server + * Component once its data fetch resolves. Without it, the previous + * route's UI freezes until the fetch finishes (the "tab switch is + * laggy" symptom). + * + * The pulse animation comes from Tailwind's animate-pulse on each + * placeholder bar; no JS, no layout shift when the real content swaps in. + */ +export default function TableSkeleton({ eyebrow, title, rows = 8 }: Props) { + return ( +
+
+
+

+ {eyebrow} +

+

+ {title} + +

+
+
+ + +
+
+
    + {Array.from({ length: rows }).map((_, i) => ( +
  • +
    +
    + + +
    + +
    +
  • + ))} +
+
+ ); +}