<Link /> component taking so long to load a client page
Unanswered
Transvaal lion posted this in #help-forum
Transvaal lionOP
Hi guys, i have a link component that is going to a client page. for some reason this takes up to 30 seconds before navigating to that pge. Why is it doing this? How can i debug / fix?
32 Replies
Asian black bear
Does this happen in production?
Transvaal lionOP
Yeah
@Asian black bear
Asian black bear
If this is happening in actual production and not local development you need to analyze your logs and tracing details where the bottleneck is. It's likely you are performing some data fetching that is blocking and taking too long. Either because it's too much data or the data source you're querying is slow etc.
@Asian black bear If this is happening in actual production and not local development you need to analyze your logs and tracing details where the bottleneck is. It's likely you are performing some data fetching that is blocking and taking too long. Either because it's too much data or the data source you're querying is slow etc.
Transvaal lionOP
But the page i am fetcing is a page that is 'use client', you're saying somewhere in a layout file there is something blocking the page from loading?
Asian black bear
Pages are not supposed to be marked with
'use client'
and even if you do, they still get prerendered on the server.Transvaal lionOP
I did <Link prerender={false} /> but still long loading time, will that change anything?
I guess I need help analyzing my logs I dont know how to do that
i am looking at vercel logs page but it doesnt show speed load times
i am looking at vercel logs page but it doesnt show speed load times
Asian black bear
No, because that prop does something entirely different. You need to share the code of the page in question, otherwise it's impossible to vaguely guess what's wrong.
Transvaal lionOP
Also thank you for helping me I really appreciat the kindness ❤️
Asian black bear
Also keep in mind you can try to to run a production build locally to verify whether the same issue is happening with a local production build.
Transvaal lionOP
Okay do you need the code to every layout file prior too? or just the page?
@Asian black bear Also keep in mind you can try to to run a production build locally to verify whether the same issue is happening with a local production build.
Transvaal lionOP
You're saying do this and just run console log in every file? It's the same speed / effectiveness as production? and will it use my local host api or my production api?
Asian black bear
I haven't said anything about any console log-ging. I was refering to you running a
next build
of your project and next start
locally to verify whether the same behavior can be observed locally while running the application against production databases/APIs etc.And in terms of code you need to share all the relevant pieces along the hierarchy of the page in question, such as parent layouts etc. which could possibly perform data fetching etc.
@Asian black bear I haven't said anything about any console log-ging. I was refering to you running a `next build` of your project and `next start` locally to verify whether the same behavior can be observed locally while running the application against production databases/APIs etc.
Transvaal lionOP
Can I send it to you in DM instead of in the server?
Asian black bear
No, I am not providing any help privately and this would also make it impossible for others to help as well.
Transvaal lionOP
import { headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { auth } from "./auth";
export const middleware = async (req: NextRequest) => {
const requestHeaders = headers();
const host = requestHeaders.get("host");
const userAgent = requestHeaders.get("user-agent") || "";
// Allow search engine bots to see the homepage
const botPatterns = [
"googlebot",
"bingbot",
"slurp",
"duckduckbot",
"baiduspider",
"yandexbot",
"facebookexternalhit",
"twitterbot",
"linkedinbot",
"curl",
];
const isBot = botPatterns.some((bot) =>
userAgent.toLowerCase().includes(bot)
);
if (isBot) {
return NextResponse.next(); // Let bots see the real page
}
// ⬅️ If we're on the signup sub-domain, do nothing here:
if (host === "affiliates.trysnow.com") {
return NextResponse.next();
}
const session = await auth();
const url = req.nextUrl.clone();
if (!session && host && host.includes("demo.growi.io")) {
url.pathname = "/api/demo-auth";
return NextResponse.redirect(url, { status: 302 });
}
if (session) {
url.pathname = "/api/auth/signin";
} else {
url.pathname = "/grow";
}
return NextResponse.redirect(url, { status: 302 });
};
export const config = {
matcher: "/",
};
import TailwindIndicator from "@/components/tailwind-indicator";
import { ThemeProvider } from "@/components/theme-provider";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { GeistSans } from "geist/font/sans";
import type { Metadata } from "next";
import Script from "next/script";
import { Toaster as Sonner } from "../components/ui/sonner";
import { Toaster } from "../components/ui/toaster";
import { cn } from "../lib/utils";
import "./globals.css";
import { Providers } from "./providers";
export const runtime = "edge";
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL as string),
title: "Growi: Content Creator Relationship Management",
description: "Manage creators, retainers, affiliates, and track content.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const startTime = Date.now();
console.log(`[${new Date().toISOString()}] RootLayout: Starting`);
const component = (
<html lang="en">
<head>
{/* Google Analytics - Load with lower priority */}
<Script
async
src="https://www.googletagmanager.com/gtag/js?id=G-XJ4TTVJ107"
strategy="lazyOnload"
/>
<Script id="google-analytics" strategy="lazyOnload">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS}');
`}
</Script>
{/* Intercom - Load with lower priority */}
<Script id="intercom-settings" strategy="lazyOnload">
{`
window.intercomSettings = {
api_base: "https://api-iam.intercom.io",
app_id: "${process.env.INTERCOM_APP_ID}",
name: null,
email: null,
user_id: null
};
`}
</Script>
<Script id="intercom-function" strategy="lazyOnload">
{`
(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/${process.env.INTERCOM_APP_ID}';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};if(document.readyState==='complete'){l();}else if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
`}
</Script>
</head>
<body
className={cn(
"min-h-screen scroll-auto font-sans antialiased selection:bg-indigo-100 selection:text-indigo-700 dark:bg-gray-950",
GeistSans.variable
)}
>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
<Providers>
{children}
<Toaster />
<Sonner position="top-center" richColors />
<TailwindIndicator />
</Providers>
</ThemeProvider>
<Analytics />
<SpeedInsights />
</body>
</html>
);
const totalTime = Date.now() - startTime;
console.log(
`[${new Date().toISOString()}] RootLayout: Total server time ${totalTime}ms`
);
return component;
}
// app/providers.jsx
"use client";
import { COOKIE_NAME } from "@/lib/admin-investigate/constants";
import InvestigationHeader from "@/lib/admin-investigate/investigation-header";
import { useTrackParams } from "@/lib/referral-tracker/use-track-params";
import { useDetectSupportParams } from "@/lib/use-detect-support-params";
import analytics from "@growi/sdk/analytics";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import Cookies from "js-cookie";
import { SessionProvider } from "next-auth/react";
import * as React from "react";
export const client = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000,
retry: false,
},
},
});
if (typeof window !== "undefined") {
analytics.init();
}
export function Providers(props: { children: React.ReactNode }) {
useTrackParams();
useDetectSupportParams();
const isInvestigating = Boolean(Cookies.get(COOKIE_NAME));
return (
<SessionProvider>
<QueryClientProvider client={client}>
{isInvestigating && <InvestigationHeader />}
{props.children}
{/* <ReactQueryDevtools
initialIsOpen={false}
buttonPosition="bottom-left"
/> */}
</QueryClientProvider>
</SessionProvider>
);
}
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import IntercomProvider from "./intercom-provider";
interface ProtectedLayoutProps {
children: React.ReactNode;
}
export default async function ProtectedLayout({
children,
}: ProtectedLayoutProps) {
const startTime = Date.now();
console.log(`[${new Date().toISOString()}] ProtectedLayout: Starting`);
const authStart = Date.now();
const session = await auth();
const authEnd = Date.now();
console.log(
`[${new Date().toISOString()}] ProtectedLayout: Auth took ${authEnd - authStart}ms`
);
if (!session?.user?.payload?.token) {
console.log(
`[${new Date().toISOString()}] ProtectedLayout: No session, redirecting`
);
redirect("/auth/signin");
}
const totalTime = Date.now() - startTime;
console.log(
`[${new Date().toISOString()}] ProtectedLayout: Total server time ${totalTime}ms`
);
return <IntercomProvider session={session}>{children}</IntercomProvider>;
}
"use client";
import { Session } from "next-auth";
import { useEffect } from "react";
export default function IntercomProvider({
session,
children,
}: {
session: Session;
children: React.ReactNode;
}) {
useEffect(() => {
if (typeof window !== "undefined" && window && window.Intercom) {
window.Intercom("update", {
email: session.user!.payload!.user_email,
name: session.user!.payload!.user_name,
user_id: session.user!.payload!.user_id.toString(),
});
}
}, []);
return <>{children}</>;
}
import { server } from "@/lib/server-fetch";
import * as React from "react";
import OrganizationUnauthorizedPage from "../../unauthorized/page";
import OrganizationSidebar from "./_components/organization-sidebar";
import TrialEndedNoPaymentMethod from "./_components/trial-ended-no-payment-method";
import OrganizationClientLayout from "./layout.client";
interface OrganizationLayoutProps {
params: { organizationSlug: string };
children: React.ReactNode;
}
// Define a type for the customer data
interface CustomerData {
bypass_subscription: boolean;
bypass_trial: boolean;
trial_days: number;
payment_method_attached: boolean;
stripe_subscription_id: string;
trial_ended: boolean;
}
// Update the response type to include customer data
interface ValidateResponse {
success: boolean;
data: Record<string, any>;
customer_data: CustomerData;
bypass: boolean;
}
export default async function OrganizationLayout({
params,
children,
}: OrganizationLayoutProps) {
const startTime = Date.now();
console.log(
`[${new Date().toISOString()}] OrganizationLayout: Starting for ${params.organizationSlug}`
);
console.log("Validating organization", params.organizationSlug);
const validateStart = Date.now();
const response = await server.organization(
params.organizationSlug,
`/validate`
);
const validateEnd = Date.now();
console.log(
`[${new Date().toISOString()}] OrganizationLayout: Validation API took ${validateEnd - validateStart}ms`
);
console.log("Validated organization", params.organizationSlug);
console.log("Response", response?.status);
if (response.status !== 200) {
console.log(
`[${new Date().toISOString()}] OrganizationLayout: Unauthorized, returning early`
);
return <OrganizationUnauthorizedPage />;
}
const parseStart = Date.now();
const data: ValidateResponse = await response.json();
const parseEnd = Date.now();
console.log(
`[${new Date().toISOString()}] OrganizationLayout: JSON parsing took ${parseEnd - parseStart}ms`
);
if (!data.success) {
console.log(
`[${new Date().toISOString()}] OrganizationLayout: Validation failed, returning early`
);
return <OrganizationUnauthorizedPage />;
}
if (!data.bypass) {
if (
data.customer_data.trial_ended &&
!data.customer_data.bypass_trial &&
!data.customer_data.payment_method_attached
) {
console.log(
`[${new Date().toISOString()}] OrganizationLayout: Trial ended, returning early`
);
return <TrialEndedNoPaymentMethod />;
}
}
const totalTime = Date.now() - startTime;
console.log(
`[${new Date().toISOString()}] OrganizationLayout: Total server time ${totalTime}ms for ${params.organizationSlug}`
);
return (
<OrganizationClientLayout
organizationSlug={params.organizationSlug}
status={response.status}
>
<OrganizationSidebar
children={children}
data={data.data}
organizationSlug={params.organizationSlug}
/>
</OrganizationClientLayout>
);
}
"use client";
import { showIntercom } from "@/components/tremor/ui/navigation/ContactButton";
import { signOut } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { ReactNode, useEffect } from "react";
const OrganizationClientLayout = ({
children,
organizationSlug,
status,
}: {
children: ReactNode;
organizationSlug: string;
status: number;
}) => {
const searchParams = useSearchParams();
useEffect(() => {
if (status === 200) return;
signOut({
callbackUrl: "/",
});
}, [status]);
// Check for showSupport parameter and open Intercom
useEffect(() => {
const showSupport =
searchParams.get("showSupport") || searchParams.get("support");
if (showSupport === "true") {
showIntercom();
}
}, [searchParams]);
return <>{children}</>;
};
export default OrganizationClientLayout;
"use client";
import {
TabNavigation,
TabNavigationLink,
} from "@/components/tremor/TabNavigation";
import { siteConfig } from "@/config/siteConfig";
import Link from "next/link";
import { usePathname } from "next/navigation";
const navigationSettings = [
{ name: "General", href: siteConfig.baseLinks.organization.settings.general },
// { name: "Billing & Usage", href: siteConfig.baseLinks.organization.settings.billing },
{ name: "Team", href: siteConfig.baseLinks.organization.settings.team },
{
name: "Integrations",
href: siteConfig.baseLinks.organization.settings.integrations,
},
{
name: "Billing",
href: siteConfig.baseLinks.organization.settings.billing,
},
{
name: "Developer",
href: siteConfig.baseLinks.organization.settings.developer,
},
{
name: "Zapier",
href: siteConfig.baseLinks.organization.settings.zapier,
},
{
name: "Slack Webhooks",
href: siteConfig.baseLinks.organization.settings.slackWebhooks,
},
{
name: "Discord Webhooks",
href: siteConfig.baseLinks.organization.settings.discordWebhooks,
},
{
name: "Audit Logs",
href: siteConfig.baseLinks.organization.settings.auditLogs,
},
];
export default function Layout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: { organizationSlug: string };
}>) {
const pathname = usePathname();
return (
<>
<h1 className="text-lg font-semibold text-gray-900 sm:text-xl dark:text-gray-50">
Settings
</h1>
<TabNavigation className="mt-4 sm:mt-6 lg:mt-10 overflow-x-auto">
{navigationSettings.map((item) => (
<TabNavigationLink
key={item.name}
asChild
active={pathname.includes(item.href)}
>
<Link
href={`/organization/${params.organizationSlug}${item.href}`}
prefetch={false}
>
{item.name}
</Link>
</TabNavigationLink>
))}
</TabNavigation>
<div className="pt-6">{children}</div>
</>
);
}
"use client";
import EmptyState from "@/components/empty-state";
import { Button } from "@/components/tremor/Button";
import { showIntercom } from "@/components/tremor/ui/navigation/ContactButton";
import useGetUser from "@/hooks/use-get-user";
import { Loader } from "lucide-react";
import BillingPortalButton from "./_components/billing-portal-button";
export default function Billing() {
const { organization, parentOrganization, isLoading, isError, error } =
useGetUser();
// Show loading while data is being fetched
if (isLoading) return <Loader className="m-auto h-4 w-4 animate-spin" />;
// Show error state if there's an error
if (isError) {
return (
<EmptyState
title="Error Loading Billing Information"
description={`There was an error loading your billing information: ${error?.message || "Unknown error"}. Please try refreshing the page.`}
>
<>
<BillingPortalButton />
<Button
asChild
variant="secondary"
className="mt-2 w-full gap-1 sm:mt-0 sm:w-fit cursor-pointer"
onClick={showIntercom}
>
<span>Contact Support</span>
</Button>
</>
</EmptyState>
);
}
if (!organization) return <Loader className="m-auto h-4 w-4 animate-spin" />;
if (organization.is_child && !parentOrganization) {
return (
<EmptyState
title="Unable to Load Billing Information"
description="There was an issue loading the parent organization's billing information. Please try refreshing the page or contact support."
>
<>
<BillingPortalButton />
<Button
asChild
variant="secondary"
className="mt-2 w-full gap-1 sm:mt-0 sm:w-fit cursor-pointer"
onClick={showIntercom}
>
<span>Contact Support</span>
</Button>
</>
</EmptyState>
);
}
const { plan_type } = parentOrganization ? parentOrganization : organization;
// Determine the plan message
let planMessage = "";
if (plan_type === "free_trial") {
planMessage = `This workspace is currently on a free trial.`;
} else if (plan_type === "agency") {
planMessage = "This workspace is on the Agency plan ($499/month base).";
} else if (plan_type === "brand") {
planMessage = "This workspace is on the Brand plan.";
} else {
planMessage = "This workspace is on an unknown plan.";
}
if (!organization.customer.payment_method_attached) {
return (
<EmptyState
title="No Payment Method on File"
description="Please add a payment method to your account in the billing portal to start processing creator payouts and paying invoices."
>
<>
<BillingPortalButton />
<Button
asChild
variant="secondary"
className="mt-2 w-full gap-1 sm:mt-0 sm:w-fit cursor-pointer"
onClick={showIntercom}
>
<span>Learn more</span>
</Button>
</>
</EmptyState>
);
}
return (
<>
<div className="rounded-lg bg-gray-50 p-6 ring-1 ring-inset ring-gray-200 dark:bg-gray-400/10 dark:ring-gray-800 flex justify-between items-end">
<div className="flex flex-col">
{/* <h4 className="text-sm font-semibold text-gray-900 dark:text-gray-50">
{planMessage}
</h4> */}
<p className="mt-1 max-w-2xl text-sm leading-6 text-gray-500">
Manage your billing and subscription details.{" "}
</p>
</div>
<BillingPortalButton buttonVariant="secondary" />
</div>
</>
);
}
@Asian black bear from top to bottom is the original root to the page i'm going to
Transvaal lionOP
Also I did next build and next start and it's blazing fast
Transvaal lionOP
Anyone?
Mugger Crocodile
refresh
@Mugger Crocodile refresh
Transvaal lionOP
What?
Transvaal lionOP
Can someone help please