From 3d053345fd165b141440f23d52b07867d7945690 Mon Sep 17 00:00:00 2001
From: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com>
Date: Wed, 8 Apr 2026 16:49:25 +0800
Subject: [PATCH] perf(web): fix slow tab switching by removing dynamic root
layout (#502)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The root layout called `await cookies()` to read the locale, which
marked the entire app as dynamic. In Next.js 16, dynamic pages have
Router Cache staleTime=0, causing a fresh RSC server roundtrip on
every navigation — the root cause of ~400ms tab switching delays.
- Remove cookies() from root layout, making it static
- Add LocaleSync client component to read locale cookie on the client
- Add loading.tsx skeleton for dashboard routes as a loading fallback
---
apps/web/app/(dashboard)/loading.tsx | 28 ++++++++++++++++++++++++++++
apps/web/app/layout.tsx | 11 ++++-------
apps/web/components/locale-sync.tsx | 20 ++++++++++++++++++++
3 files changed, 52 insertions(+), 7 deletions(-)
create mode 100644 apps/web/app/(dashboard)/loading.tsx
create mode 100644 apps/web/components/locale-sync.tsx
diff --git a/apps/web/app/(dashboard)/loading.tsx b/apps/web/app/(dashboard)/loading.tsx
new file mode 100644
index 00000000..8e96cdd5
--- /dev/null
+++ b/apps/web/app/(dashboard)/loading.tsx
@@ -0,0 +1,28 @@
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function DashboardLoading() {
+ return (
+
+ {/* Header skeleton */}
+
+
+
+
+ {/* Toolbar skeleton */}
+
+
+
+
+ {/* Content skeleton */}
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index c1df5730..26c41c34 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,5 +1,4 @@
import type { Metadata, Viewport } from "next";
-import { cookies } from "next/headers";
import { Geist, Geist_Mono } from "next/font/google";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
@@ -8,6 +7,7 @@ import { QueryProvider } from "@core/provider";
import { AuthInitializer } from "@/features/auth";
import { WSProvider } from "@/features/realtime";
import { ModalRegistry } from "@/features/modals";
+import { LocaleSync } from "@/components/locale-sync";
import "./globals.css";
const geist = Geist({ subsets: ["latin"], variable: "--font-sans" });
@@ -51,22 +51,19 @@ export const metadata: Metadata = {
},
};
-export default async function RootLayout({
+export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
- const cookieStore = await cookies();
- const locale = cookieStore.get("multica-locale")?.value;
- const lang = locale === "zh" ? "zh" : "en";
-
return (
+
diff --git a/apps/web/components/locale-sync.tsx b/apps/web/components/locale-sync.tsx
new file mode 100644
index 00000000..e223adb3
--- /dev/null
+++ b/apps/web/components/locale-sync.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { useEffect } from "react";
+
+/**
+ * Reads the locale cookie on the client and updates .
+ * This avoids calling cookies() in the root Server Component layout,
+ * which would mark the entire app as dynamic and disable the Router Cache.
+ */
+export function LocaleSync() {
+ useEffect(() => {
+ const match = document.cookie.match(/(?:^|;\s*)multica-locale=(\w+)/);
+ const locale = match?.[1];
+ if (locale === "zh") {
+ document.documentElement.lang = "zh";
+ }
+ }, []);
+
+ return null;
+}