Help! 30 seconds to first paint
I must be doing something upside down and inside out. When loading fresh (no local cache, etc.), my app can take up to 30s before it's actually rendered.
Instrumenting the performance hasn't revealed anything obvious. In fact, some of the time the browser reports that the initial frame renders in <1s although nothing is visible until 20-30seconds later.
Any tips on how to investigate and debug this would be super helpful! Here's an example of a typical route in my app:
// router.ts
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { QueryClient } from "@tanstack/react-query";
import { routerWithQueryClient } from "@tanstack/react-router-with-query";
import { ConvexQueryClient } from "@convex-dev/react-query";
import { routeTree } from "./routeTree.gen";
import { ConvexAuthProvider } from "@convex-dev/auth/react";
export function createRouter() {
let CONVEX_URL = import.meta.env.VITE_CONVEX_URL_DEV;
const convexQueryClient = new ConvexQueryClient(CONVEX_URL);
const queryClient: QueryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
},
},
});
convexQueryClient.connect(queryClient);
const router = routerWithQueryClient(
createTanStackRouter({
routeTree,
defaultPreload: "intent",
defaultPendingMinMs: 0, //failed attempt to fix slow render
defaultPreloadStaleTime: 0, //failed attempt to fix slow render
defaultNotFoundComponent: () => <div>404</div>,
context: {
queryClient,
auth: undefined!,
},
Wrap: ({ children }) => {
return (
<ConvexAuthProvider client={convexQueryClient.convexClient}>
{children}
</ConvexAuthProvider>
);
},
}),
queryClient
);
return router;
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof createRouter>;
}
}
// router.ts
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { QueryClient } from "@tanstack/react-query";
import { routerWithQueryClient } from "@tanstack/react-router-with-query";
import { ConvexQueryClient } from "@convex-dev/react-query";
import { routeTree } from "./routeTree.gen";
import { ConvexAuthProvider } from "@convex-dev/auth/react";
export function createRouter() {
let CONVEX_URL = import.meta.env.VITE_CONVEX_URL_DEV;
const convexQueryClient = new ConvexQueryClient(CONVEX_URL);
const queryClient: QueryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
},
},
});
convexQueryClient.connect(queryClient);
const router = routerWithQueryClient(
createTanStackRouter({
routeTree,
defaultPreload: "intent",
defaultPendingMinMs: 0, //failed attempt to fix slow render
defaultPreloadStaleTime: 0, //failed attempt to fix slow render
defaultNotFoundComponent: () => <div>404</div>,
context: {
queryClient,
auth: undefined!,
},
Wrap: ({ children }) => {
return (
<ConvexAuthProvider client={convexQueryClient.convexClient}>
{children}
</ConvexAuthProvider>
);
},
}),
queryClient
);
return router;
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof createRouter>;
}
}
// routes/_authenticated/route.tsx (VERY SLOW)
import { AppSidebar } from "@/components/app-sidebar";
import { TSRBreadCrumbs } from "@/components/breadcrumbs/breadcrumbs";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { api } from "@/convex/_generated/api";
import { withAuth } from "@/hooks/with-auth";
import { convexQuery } from "@convex-dev/react-query";
import { createFileRoute, Outlet, useNavigate } from "@tanstack/react-router";
import { Authenticated } from "convex/react";
import { useEffect } from "react";
export const Route = createFileRoute("/_authenticated")({
component: AppWrapper,
loader(ctx) {
ctx.context.queryClient.ensureQueryData(
convexQuery(api.authentication.queries.getMe, {})
);
},
});
function AppWrapper() {
const { isAuthenticated, isLoading } = withAuth();
const navigate = useNavigate({ from: "/" });
useEffect(() => {
if (!isAuthenticated && !isLoading) {
navigate({
to: "/login",
});
}
}, [isAuthenticated, isLoading, navigate]);
if (isLoading) {
return null;
}
return (
<Authenticated>
<SidebarProvider name="main">
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<TSRBreadCrumbs />
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-0">
<main>
<Outlet />
</main>
</div>
</SidebarInset>
</SidebarProvider>
</Authenticated>
);
}
// routes/_authenticated/route.tsx (VERY SLOW)
import { AppSidebar } from "@/components/app-sidebar";
import { TSRBreadCrumbs } from "@/components/breadcrumbs/breadcrumbs";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { api } from "@/convex/_generated/api";
import { withAuth } from "@/hooks/with-auth";
import { convexQuery } from "@convex-dev/react-query";
import { createFileRoute, Outlet, useNavigate } from "@tanstack/react-router";
import { Authenticated } from "convex/react";
import { useEffect } from "react";
export const Route = createFileRoute("/_authenticated")({
component: AppWrapper,
loader(ctx) {
ctx.context.queryClient.ensureQueryData(
convexQuery(api.authentication.queries.getMe, {})
);
},
});
function AppWrapper() {
const { isAuthenticated, isLoading } = withAuth();
const navigate = useNavigate({ from: "/" });
useEffect(() => {
if (!isAuthenticated && !isLoading) {
navigate({
to: "/login",
});
}
}, [isAuthenticated, isLoading, navigate]);
if (isLoading) {
return null;
}
return (
<Authenticated>
<SidebarProvider name="main">
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<TSRBreadCrumbs />
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-0">
<main>
<Outlet />
</main>
</div>
</SidebarInset>
</SidebarProvider>
</Authenticated>
);
}
1 Reply
optimistic-goldOP•8mo ago
// routes/_authenticated/campaigns/create/index.tsx
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { api } from "@/convex/_generated/api";
import { convexQuery } from "@convex-dev/react-query";
import CampaignEditor from "@/components/campaigns/editor/campaign-editor";
import { useCampaignState } from "@/components/campaigns/editor/hooks/use-campaign-state";
export const Route = createFileRoute("/_authenticated/campaigns/create/")({
component: CampaignsEdit,
loader(ctx) {
// Prefetch required data for editor
ctx.context.queryClient.ensureQueryData(
convexQuery(api.mailchimp.segments.queries.getMergedSegments, {})
);
ctx.context.queryClient.ensureQueryData(
convexQuery(api.mailchimp.lists.queries.getForCurrentUser, {})
);
},
});
function CampaignsEdit() {
return <CampaignEditor />;
}
// routes/_authenticated/campaigns/create/index.tsx
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { api } from "@/convex/_generated/api";
import { convexQuery } from "@convex-dev/react-query";
import CampaignEditor from "@/components/campaigns/editor/campaign-editor";
import { useCampaignState } from "@/components/campaigns/editor/hooks/use-campaign-state";
export const Route = createFileRoute("/_authenticated/campaigns/create/")({
component: CampaignsEdit,
loader(ctx) {
// Prefetch required data for editor
ctx.context.queryClient.ensureQueryData(
convexQuery(api.mailchimp.segments.queries.getMergedSegments, {})
);
ctx.context.queryClient.ensureQueryData(
convexQuery(api.mailchimp.lists.queries.getForCurrentUser, {})
);
},
});
function CampaignsEdit() {
return <CampaignEditor />;
}
ctx.context.queryClient.ensureQueryData... await ctx.context.queryClient.ensureQueryData
Centralized redundant queries into a single hook
Moved some queries to the narrowest route that requires them